1
0
Files
spring-grpc-boot4/results-full.txt
Ankur 8547981634 Spring gRPC on Spring Boot 4: four call types, exception mapping, and a symptom-first troubleshooting suite
Companion code for the ankurm.com guide. Verified on Spring Boot 4.1.0, spring-grpc 1.1.0,
grpc-java 1.80.0, protobuf-java 4.34.2, JDK 25.0.3. results-full.txt is unedited mvn test
output: 11 tests, 0 failures. No installs needed - protoc and the gRPC codegen plugin resolve
as Maven artifacts, and every test uses the in-process transport.

_01_basics       all four call types and how their failure modes differ: iterator semantics on
                 server streaming, the half-close that client streaming hangs without, and the
                 independence of the two streams in bidi.
_02_troubleshooting
                 four failures reproduced then fixed - the 4 MB message limit and which side
                 enforces it, the absent default deadline, cancellation that never interrupts a
                 thread, and errors that arrive as UNKNOWN with no description.
_03_exceptions   @GrpcAdvice / @GrpcExceptionHandler: domain exception to NOT_FOUND with
                 trailers, validation to INVALID_ARGUMENT, and a catch-all that returns a
                 deliberate INTERNAL without leaking the original message across the boundary.

docs/01          symptom-first troubleshooting index, each entry marked [tested] or [documented]
docs/02          complete spring.grpc.* property reference, and the split between the Boot 4.1
                 integration (org.springframework.boot, version 4.1.0, owns properties and
                 auto-configuration) and the Spring gRPC project (org.springframework.grpc,
                 version 1.1.0, owns the programming model). The older standalone
                 spring-grpc-spring-boot-starter stops at 1.0.3 and is what most search results
                 describe.

Findings worth the commit message
  - The in-process transport CANNOT enforce message size limits: it passes messages by
    reference and never serialises them. A 4 MB + 1 KB message goes through cleanly in tests
    and fails in production with RESOURCE_EXHAUSTED. The test asserts this rather than
    pretending otherwise. Same blind spot covers compression, TLS, keepalive and LB.
  - Two property names cost real time while writing this:
    spring.grpc.server.inprocess.name (not in-process) and
    spring.grpc.client.channel.<n>.target (singular channel, and target not address).
    The second failure surfaces as UnknownHostException on the channel NAME.
  - gRPC still has no default deadline, and cancellation only sets a Context flag.
2026-07-31 23:05:02 +05:30

271 lines
36 KiB
Plaintext

mvn.cmd : WARNING: A restricted method in java.lang.System has been called
At line:5 char:1
+ & $mvn -B test *> results-full.txt
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (WARNING: A rest...has been called:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
WARNING: java.lang.System::load has been called by org.fusesource.jansi.internal.JansiLoader in an unnamed module
(file:/C:/Users/Ankur/ankurm-blog-tools/apache-maven-3.9.9/lib/jansi-2.4.1.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by
com.google.common.util.concurrent.AbstractFuture$UnsafeAtomicHelper
(file:/C:/Users/Ankur/ankurm-blog-tools/apache-maven-3.9.9/lib/guava-33.2.1-jre.jar)
WARNING: Please consider reporting this to the maintainers of class
com.google.common.util.concurrent.AbstractFuture$UnsafeAtomicHelper
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------< com.ankurm.grpc:spring-grpc-boot4 >------------------
[INFO] Building Spring gRPC with Spring Boot 4 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[WARNING] Parameter 'protocVersion' is unknown for plugin 'protobuf-maven-plugin:5.1.4:generate (generate)'
[WARNING] Parameter 'binaryMavenPlugins' is unknown for plugin 'protobuf-maven-plugin:5.1.4:generate (generate)'
[WARNING] Parameter 'protocVersion' is unknown for plugin 'protobuf-maven-plugin:5.1.4:generate (default)'
[WARNING] Parameter 'binaryMavenPlugins' is unknown for plugin 'protobuf-maven-plugin:5.1.4:generate (default)'
[INFO]
[INFO] --- protobuf:5.1.4:generate (generate) @ spring-grpc-boot4 ---
[INFO] Registering "C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4\target\generated-sources\protobuf" as a Maven main source root for compiler plugins
[INFO] Registering "C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4\target\generated-sources\protobuf" as a Maven main source root for compiler plugins
[INFO] Found 1 proto source file and 0 descriptor files to compile, but all are up-to-date so none will be built this time
[INFO] NOTHING TO DO: There is nothing to do. If this is unexpected, review the above logs for more details.
[INFO]
[INFO] --- protobuf:5.1.4:generate (default) @ spring-grpc-boot4 ---
[INFO] Registering "C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4\target\generated-sources\protobuf" as a Maven main source root for compiler plugins
[INFO] Registering "C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4\target\generated-sources\protobuf" as a Maven main source root for compiler plugins
[INFO] Found 1 proto source file and 0 descriptor files to compile, but all are up-to-date so none will be built this time
[INFO] NOTHING TO DO: There is nothing to do. If this is unexpected, review the above logs for more details.
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ spring-grpc-boot4 ---
[INFO] skip non existing resourceDirectory C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4\src\main\resources
[INFO] skip non existing resourceDirectory C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4\src\main\resources
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ spring-grpc-boot4 ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 20 source files with javac [debug parameters release 25] to target\classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ spring-grpc-boot4 ---
[INFO] skip non existing resourceDirectory C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4\src\test\resources
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ spring-grpc-boot4 ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 5 source files with javac [debug parameters release 25] to target\test-classes
[INFO]
[INFO] --- surefire:3.5.6:test (default-test) @ spring-grpc-boot4 ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.ankurm.grpc._01_basics.FourCallTypesTest
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.0)
2026-07-31T23:03:43.299+05:30 INFO 36456 --- [ main] c.a.grpc._01_basics.FourCallTypesTest : Starting FourCallTypesTest using Java 25.0.3 with PID 36456 (started by Ankur in C:\Users\Ankur\ankurm-blog-tools\projects\spring-grpc-boot4)
2026-07-31T23:03:43.303+05:30 INFO 36456 --- [ main] c.a.grpc._01_basics.FourCallTypesTest : No active profile set, falling back to 1 default profile: "default"
2026-07-31T23:03:44.245+05:30 INFO 36456 --- [ main] o.s.grpc.server.NettyGrpcServerFactory : Registered gRPC service: com.ankurm.grpc.orders.OrderService
2026-07-31T23:03:44.245+05:30 INFO 36456 --- [ main] o.s.grpc.server.NettyGrpcServerFactory : Registered gRPC service: grpc.reflection.v1.ServerReflection
2026-07-31T23:03:44.245+05:30 INFO 36456 --- [ main] o.s.grpc.server.NettyGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health
2026-07-31T23:03:44.445+05:30 INFO 36456 --- [ main] o.s.g.s.lifecycle.GrpcServerLifecycle : gRPC Server started, listening on address: [/[0:0:0:0:0:0:0:0]:9090], port: 9090
2026-07-31T23:03:44.447+05:30 INFO 36456 --- [ main] o.s.g.server.InProcessGrpcServerFactory : Registered gRPC service: com.ankurm.grpc.orders.OrderService
2026-07-31T23:03:44.448+05:30 INFO 36456 --- [ main] o.s.g.server.InProcessGrpcServerFactory : Registered gRPC service: grpc.reflection.v1.ServerReflection
2026-07-31T23:03:44.448+05:30 INFO 36456 --- [ main] o.s.g.server.InProcessGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health
2026-07-31T23:03:44.449+05:30 INFO 36456 --- [ main] o.s.g.s.lifecycle.GrpcServerLifecycle : gRPC Server started, listening on address: [orders-test], port: -1
2026-07-31T23:03:44.451+05:30 INFO 36456 --- [ main] c.a.grpc._01_basics.FourCallTypesTest : Started FourCallTypesTest in 1.418 seconds (process running for 2.636)
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of
the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation:
https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap
classpath has been appended
WARNING: A Java agent has been loaded dynamically
(C:\Users\Ankur\.m2\repository\net\bytebuddy\byte-buddy-agent\1.18.10\byte-buddy-agent-1.18.10.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
==============================================================================
Client streaming: many requests, one response
==============================================================================
accepted=3 totalCents=3000
>> requests.onCompleted() is the half-close. Without it the call hangs until
>> the deadline -- and if you did not set a deadline, it hangs forever.
==============================================================================
Unary: one request, one response
==============================================================================
id=abc customer=ankur amountCents=1999 status=PENDING
>> A @Service extending the generated ImplBase is registered automatically:
>> it is a BindableService, and Boot 4 wires every such bean into the server.
==============================================================================
Bidirectional streaming: many requests, many responses
==============================================================================
sent 4, received 4, all status=PAID
>> Request and response streams are INDEPENDENT. The server may respond
>> before you finish sending, or not at all until you half-close. Do not
>> assume a request/response pairing -- that is your protocol's job, not gRPC's.
>>
>> StreamObserver is NOT thread-safe. Calling onNext from two threads without
>> synchronisation corrupts the stream, and the symptom is usually a
>> deserialization error on the far side rather than anything pointing here.
==============================================================================
Server streaming: one request, many responses
==============================================================================
received 5 messages: [order-0, order-1, order-2, order-3, order-4]
>> The blocking stub returns an Iterator. Each next() may block, and an
>> exception surfaces mid-iteration -- so a try/catch around the loop body
>> is not the same as one around the call. Wrap the whole iteration.
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.840 s -- in com.ankurm.grpc._01_basics.FourCallTypesTest
[INFO] Running com.ankurm.grpc._02_troubleshooting.HardToDiagnoseTest
==============================================================================
4. Errors: UNKNOWN by default, useful only if you make them so
==============================================================================
-- A handler that fails with a proper status and trailing metadata -----------
code : FAILED_PRECONDITION
description : order o-42 is locked
x-failure-reason : ORDER_LOCKED
x-retry-after-seconds : 30
[PROBLEM] If the handler had simply thrown IllegalStateException, the caller would
[PROBLEM] have received UNKNOWN with a null description. Nothing actionable at all.
[FIX] Throw StatusRuntimeException with a code that means something:
[FIX] NOT_FOUND / INVALID_ARGUMENT / FAILED_PRECONDITION -- do NOT retry
[FIX] UNAVAILABLE / DEADLINE_EXCEEDED / RESOURCE_EXHAUSTED -- retry may help
[FIX] Attach machine-readable context as trailers, not in the description string.
>> The status code is the contract. Clients, retry policies, circuit breakers
>> and dashboards all key off it, so choosing it carelessly makes every
>> downstream behaviour wrong -- most damagingly, it makes non-retryable
>> failures look retryable and turns one bad request into a storm.
==============================================================================
1. The 4 MB limit -- and why your integration tests will not catch it
==============================================================================
-- Sending a 4 MB + 1 KB response over the IN-PROCESS transport --------------
received 4,195,328 bytes -- no error
[PROBLEM] This message is OVER the 4 MB default limit and nothing complained.
[PROBLEM] The in-process transport passes message objects BY REFERENCE -- it never
[PROBLEM] serialises them -- so there are no bytes to measure and the limit cannot
[PROBLEM] be applied. Over a real Netty transport the same call fails with:
[PROBLEM] RESOURCE_EXHAUSTED: gRPC message exceeds maximum size 4194304
>> This is a genuinely nasty testing trap. In-process transport is otherwise
>> an excellent test transport -- interceptors, statuses, deadlines and
>> cancellation all behave correctly -- but message size limits, compression
>> and anything else that depends on the wire format do NOT apply.
>> A payload-size regression will pass every in-process test you have.
>>
>> Test size limits against a real port, or not at all -- but do not believe
>> a green in-process suite on this point.
-- The fix in production, for when you do hit it -----------------------------
[FIX] The limit applies to the RECEIVER, and the two sides are configured separately:
[FIX] large RESPONSE -> client inbound limit
[FIX] spring.grpc.client.channel.<name>.inbound.message.max-size=16MB
[FIX] large REQUEST -> server inbound limit
[FIX] spring.grpc.server.inbound.message.max-size=16MB
[FIX] Per call, without touching configuration:
[FIX] stub.withMaxInboundMessageSize(16 * 1024 * 1024)
[FIX]
[FIX] Half of all 'I raised the limit and it still fails' reports are this
[FIX] asymmetry: raising it on the server does nothing for a large RESPONSE.
[FIX]
[FIX] Do not raise it globally. A large limit turns a malformed request into an OOM.
[FIX] Prefer streaming: the limit is per MESSAGE, so 1000 small messages are fine.
==============================================================================
3. Cancellation does not interrupt your thread -- you must check for it
==============================================================================
-- Client starts a 200-message stream, then cancels after a few messages -----
received 3 messages, now cancelling
2026-07-31T23:03:45.543+05:30 INFO 36456 --- [ault-executor-0] c.ankurm.grpc.orders.OrderServiceImpl : client cancelled after 3 of 200 messages -- stopping work
server noticed cancellation after 3 of 200 messages
[FIX] The server loop checks Context.current().isCancelled() between messages and
[FIX] returns. Without that check it would have produced all 200 messages into a
[FIX] dead stream, doing every database read and every serialization for nothing.
>> Any server handler that loops, or that does work in stages, should check
>> Context.current().isCancelled(). For blocking work, propagate the Context
>> to worker threads with Context.current().wrap(runnable) -- otherwise the
>> cancellation flag is invisible to them.
>>
>> Also: after cancellation the stream is CLOSED. Calling onNext/onCompleted
>> on it throws IllegalStateException. Just return.
==============================================================================
2. gRPC has NO default deadline -- a hung server hangs you forever
==============================================================================
-- A call with an explicit deadline shorter than the server takes ------------
DEADLINE_EXCEEDED after 304 ms
[PROBLEM] Without .withDeadlineAfter(...) that call would have blocked for the full
[PROBLEM] 3 seconds -- and if the server never responded, forever.
-- The fix -------------------------------------------------------------------
[FIX] Per call: stub.withDeadlineAfter(2, TimeUnit.SECONDS)
[FIX] Per channel: register a DefaultDeadlineSetupClientInterceptor, or set
[FIX] spring.grpc.client.channel.<name>.default.deadline=2s
[FIX] Treat a missing deadline as a code-review failure, like a missing timeout
[FIX] on an HTTP client.
>> A deadline is ABSOLUTE and propagates: if service A calls B with 2s
>> remaining, B sees 2s, not a fresh 2s. That is the property that stops a
>> deep call chain from multiplying its timeouts -- and the reason you should
>> set the deadline at the EDGE, not re-set it at every hop.
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.532 s -- in com.ankurm.grpc._02_troubleshooting.HardToDiagnoseTest
[INFO] Running com.ankurm.grpc._03_exceptions.ExceptionMappingTest
==============================================================================
Exception mapping: a domain exception becomes NOT_FOUND
==============================================================================
code : NOT_FOUND
description : no order with id missing-42
x-failure-reason : ORDER_NOT_FOUND
[FIX] The service method just did: throw new OrderNotFoundException(id);
[FIX] @GrpcAdvice + @GrpcExceptionHandler(OrderNotFoundException.class) did the rest.
==============================================================================
Exception mapping: IllegalArgumentException becomes INVALID_ARGUMENT
==============================================================================
code : INVALID_ARGUMENT
description : order id must not be blank
>> NOT_FOUND and INVALID_ARGUMENT are both in the 'never retry' family.
>> A client that retries either is burning capacity on an answer that
>> will not change. That distinction is the whole reason to map.
==============================================================================
Exception mapping: the catch-all, and what it deliberately hides
==============================================================================
code : INTERNAL
description : internal error
[PROBLEM] Without the advice this would have been UNKNOWN with a NULL description --
[PROBLEM] no code to branch on and no message to read.
[FIX] With it, callers get a deliberate INTERNAL, and the sensitive text stays
[FIX] server-side where it belongs. Log it with a correlation id and return that.
>> Order matters: @GrpcExceptionHandler(Exception.class) is a catch-all, so
>> more specific handlers must exist for the types you care about -- they are
>> matched most-specific-first, exactly like @ExceptionHandler.
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.034 s -- in com.ankurm.grpc._03_exceptions.ExceptionMappingTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10.913 s
[INFO] Finished at: 2026-07-31T23:03:46+05:30
[INFO] ------------------------------------------------------------------------