Skip to content

Add GraphQL subscription support over graphql-transport-ws - #3512

Merged
velo merged 4 commits into
masterfrom
graphql-subscriptions
Aug 11, 2026
Merged

Add GraphQL subscription support over graphql-transport-ws#3512
velo merged 4 commits into
masterfrom
graphql-subscriptions

Conversation

@velo

@velo velo commented Aug 10, 2026

Copy link
Copy Markdown
Member

feign-graphql could generate model types for a subscription document, but had no way to execute
one: 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

GraphqlSubscriptionClient wraps the configured Client. Requests whose operation is a
subscription are executed over
graphql-transport-ws using the
JDK's java.net.http.WebSocket; everything else is a straight delegate.execute(...), so queries
and 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. Client
and server messages are records encoded and decoded through the configured JsonCodec, so no JSON
is hand-built; each record carries every component the protocol defines for its shape, which
matters because new JacksonCodec() uses a raw ObjectMapper with FAIL_ON_UNKNOWN_PROPERTIES
enabled.

Return types

The declared return type picks how many events arrive and whether the call blocks:

Return type Events Behaviour
T first only blocks until the first event, then unsubscribes
Optional<T> first only as above, empty if the server completes without one
CompletableFuture<T> first only returns immediately, completes with the first event
Stream<T> all returns once subscribed, then blocks on each element
Flow.Publisher<T> all returns immediately, elements are pushed to the subscriber

Flow.Publisher is java.util.concurrent.Flow.Publisher, so Reactor and RxJava adapt it without
adding a dependency here.

Timeouts

java.util.stream.Stream has no timeout facility of its own, so the blocking forms are bounded by
an event timeout — 60s by default, overridable on the capability, Duration.ZERO waits forever:

new GraphqlCapability(new JacksonCodec(), Duration.ofSeconds(5))

It applies per event rather than to the subscription as a whole, and raises
SocketTimeoutException. Flow.Publisher and CompletableFuture are deliberately unbounded by
it, since their caller already owns the deadline; cancelling either closes the socket.

Concurrency and resource behaviour

  • Backpressure. Reads are demand-driven: the client asks the socket for another frame only once
    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.
  • Thread governance. The workers behind Flow.Publisher and CompletableFuture run on a
    bounded daemon pool owned by the capability, injectable via a constructor. Nothing touches
    ForkJoinPool.commonPool().
  • Cancellation. Closing a Stream, cancelling a Flow.Subscription or cancelling a
    CompletableFuture all send complete and close the socket. unsubscribe is idempotent.
  • Send failures. The serialized send chain reports a failed sendText to the consumer instead
    of poisoning the chain and silently dropping every later frame.

Also fixed

  • GraphqlSchemaProcessor only unwrapped List when deriving the result type name, so
    Stream<Price> would have generated a record named Stream. It now unwraps List, Stream and
    Publisher from one shared set.
  • GraphqlDecoder now shares one unwrap between HTTP responses and subscription payloads, so
    data unwrapping and errorsGraphqlErrorException behave identically on both paths.

Notes for review

  • The subscription pseudo-response uses status 204 so feign's logger does not drain and replace the
    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, which
    covers void methods (never decoded) rather than leaking the socket.
  • One connection per subscription; multiplexing several over one socket is not implemented.
  • The WebSocket uses a JDK HttpClient, so custom SSL/proxy config on an OkHttp or Apache client
    does not carry over — only request headers do.
  • MDC and other ThreadLocal context are not propagated to the asynchronous forms' workers. No
    context is set, so nothing leaks, but logging from a Flow.Publisher or CompletableFuture
    subscription loses correlation ids. A propagation hook belongs in feign core rather than here.

Testing

mvn install green: 73 tests in feign-graphql, 65 in feign-graphql-apt.

GraphqlSubscriptionTest (19) covers each return type, the handshake, server error messages and
errors inside 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, and
queries plus subscriptions sharing one client.

GraphqlSubscriptionConcurrencyTest (3) opens 24 subscriptions simultaneously on their own sockets,
sharing one capability, one ObjectMapper and one worker pool: stream isolation, publisher
delivery, 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.

@velo
velo force-pushed the graphql-subscriptions branch 2 times, most recently from 9d52a52 to 32c7e5e Compare August 10, 2026 21:37
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
@velo
velo force-pushed the graphql-subscriptions branch from 32c7e5e to 0852535 Compare August 10, 2026 22:00
velo added 3 commits August 10, 2026 19:43
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>
@velo
velo merged commit 77f9f64 into master Aug 11, 2026
4 checks passed
@velo
velo deleted the graphql-subscriptions branch August 11, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant