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.
271 lines
36 KiB
Plaintext
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] ------------------------------------------------------------------------
|