Add GraphQL subscription support over graphql-transport-ws - #3512
Merged
Conversation
velo
force-pushed
the
graphql-subscriptions
branch
2 times, most recently
from
August 10, 2026 21:37
9d52a52 to
32c7e5e
Compare
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
velo
force-pushed
the
graphql-subscriptions
branch
from
August 10, 2026 22:00
32c7e5e to
0852535
Compare
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feign-graphqlcould generate model types for asubscriptiondocument, but had no way to executeone: the runtime was a single POST with a single JSON response. This adds the transport.
Three commits: the feature, then the concurrency hardening from a review pass, then the test that
covers it under concurrent load.
Transport
GraphqlSubscriptionClientwraps the configuredClient. Requests whose operation is asubscriptionare executed overgraphql-transport-ws using the
JDK's
java.net.http.WebSocket; everything else is a straightdelegate.execute(...), so queriesand mutations keep their client, retryer, interceptors and timeouts unchanged. Queries and
subscriptions can share one interface.
No new dependencies. The endpoint is the target URL with its scheme swapped to
ws/wss. Clientand server messages are records encoded and decoded through the configured
JsonCodec, so no JSONis hand-built; each record carries every component the protocol defines for its shape, which
matters because
new JacksonCodec()uses a rawObjectMapperwithFAIL_ON_UNKNOWN_PROPERTIESenabled.
Return types
The declared return type picks how many events arrive and whether the call blocks:
TOptional<T>CompletableFuture<T>Stream<T>Flow.Publisher<T>Flow.Publisherisjava.util.concurrent.Flow.Publisher, so Reactor and RxJava adapt it withoutadding a dependency here.
Timeouts
java.util.stream.Streamhas no timeout facility of its own, so the blocking forms are bounded byan event timeout — 60s by default, overridable on the capability,
Duration.ZEROwaits forever:It applies per event rather than to the subscription as a whole, and raises
SocketTimeoutException.Flow.PublisherandCompletableFutureare deliberately unbounded byit, since their caller already owns the deadline; cancelling either closes the socket.
Concurrency and resource behaviour
the consumer has taken the previous event, so a slow consumer pushes back on the server rather
than growing a queue. The queue is bounded as a backstop and fails loudly rather than silently.
Flow.PublisherandCompletableFuturerun on abounded daemon pool owned by the capability, injectable via a constructor. Nothing touches
ForkJoinPool.commonPool().Stream, cancelling aFlow.Subscriptionor cancelling aCompletableFutureall sendcompleteand close the socket.unsubscribeis idempotent.sendTextto the consumer insteadof poisoning the chain and silently dropping every later frame.
Also fixed
GraphqlSchemaProcessoronly unwrappedListwhen deriving the result type name, soStream<Price>would have generated a record namedStream. It now unwrapsList,StreamandPublisherfrom one shared set.GraphqlDecodernow shares oneunwrapbetween HTTP responses and subscription payloads, sodataunwrapping anderrors→GraphqlErrorExceptionbehave identically on both paths.Notes for review
body, which would drop the live subscription. It never crosses the wire.
Response.Body.close()tears down any subscription the decoder did not take ownership of, whichcovers
voidmethods (never decoded) rather than leaking the socket.HttpClient, so custom SSL/proxy config on an OkHttp or Apache clientdoes not carry over — only request headers do.
ThreadLocalcontext are not propagated to the asynchronous forms' workers. Nocontext is set, so nothing leaks, but logging from a
Flow.PublisherorCompletableFuturesubscription loses correlation ids. A propagation hook belongs in feign core rather than here.
Testing
mvn installgreen: 73 tests infeign-graphql, 65 infeign-graphql-apt.GraphqlSubscriptionTest(19) covers each return type, the handshake, servererrormessages anderrorsinside payloads, the event timeout on both blocking forms, async forms staying unbounded,messages for another operation being ignored, socket teardown on every path including
void, andqueries plus subscriptions sharing one client.
GraphqlSubscriptionConcurrencyTest(3) opens 24 subscriptions simultaneously on their own sockets,sharing one capability, one
ObjectMapperand one worker pool: stream isolation, publisherdelivery, and abandoning a stream mid-flight while the server is still pushing.
Two of the new tests were mutation-checked rather than trusted for being green — removing the
demand call stalls the slow-consumer test, and making the event queue static fails the isolation
test.