skip to content

You need cross-cutting logic (correlation IDs, response headers, auth context) applied consistently across HTTP, WebSocket, and RSocket GraphQL transports. How does Spring for GraphQL's interception model let you do this, and what are the ordering and transport-specific trade-offs?

level: principalimportance: nice to knowfreq 15%

answer

  1. WebGraphQlInterceptor = HTTP + WebSocket
  2. RSocketGraphQlInterceptor = RSocket
  3. chain of filters, @Order low = outermost
  4. converge shared state in GraphQLContext
  5. HTTP response headers have no RSocket analogue

basics

~20 s

Use WebGraphQlInterceptor for HTTP and WebSocket, and RSocketGraphQlInterceptor for RSocket. Each is a chained filter around execution where you read/write context and headers. Control order with Ordered/@Order; put shared logic in the GraphQLContext so all transports converge.

solid answer

~50 s

Spring for GraphQL splits interception by transport: WebGraphQlInterceptor covers both HTTP and WebSocket (they share the Web abstraction), while RSocketGraphQlInterceptor handles RSocket. Each interceptor is a link in an ordered chain wrapping graphql-java execution; you implement intercept(request, chain), enrich the GraphQLContext on the way in, and post-process the WebGraphQlResponse on the way out. Order via Ordered/@Order — lower value is outermost, seeing the request first and response last. The design trade-off: web-only concerns (HTTP response headers, cookies) live in WebGraphQlInterceptor and have no RSocket analogue, so keep transport-neutral logic (correlation ID generation, tenant/auth context) as small helpers that write to the GraphQLContext, and keep transport-specific I/O in the matching interceptor. Also remember WebSocket only exposes headers at handshake/connection_init, so per-request header reads that work over HTTP won't work mid-connection — resolve those at connect time.

code

java · 28 lines
java
// Transport-neutral logic, reused by both interceptors
static Map<String,Object> baseContext(HttpHeaders headersOrNull) {
    String cid = Optional.ofNullable(headersOrNull)
        .map(h -> h.getFirst("X-Correlation-Id"))
        .orElse(UUID.randomUUID().toString());
    return Map.of("correlationId", cid);
}

@Component
@Order(0) // outermost: wraps everything
class WebTracingInterceptor implements WebGraphQlInterceptor {
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest req, Chain chain) {
        req.configureExecutionInput((in, b) ->
            b.graphQLContext(baseContext(req.getHeaders())).build());
        return chain.next(req).doOnNext(resp ->            // web-only: echo header
            resp.getResponseHeaders().add("X-Correlation-Id",
                (String) resp.getExecutionInput().getGraphQLContext().get("correlationId")));
    }
}

@Component
class RSocketTracingInterceptor implements RSocketGraphQlInterceptor {
    public Mono<RSocketGraphQlResponse> intercept(RSocketGraphQlRequest req, Chain chain) {
        req.configureExecutionInput((in, b) ->
            b.graphQLContext(baseContext(null)).build()); // no HTTP headers over RSocket
        return chain.next(req);
    }
}

go deeper

for a junior

Know there are transport-specific interceptors and shared state goes in the GraphQLContext.

for a middle

Explain WebGraphQlInterceptor covers HTTP+WebSocket, RSocket has its own, and ordering via @Order.

for a senior

Detail onion-chain ordering, response post-processing asymmetry, and WebSocket handshake-only headers.

for a principal

Architect transport-neutral cross-cutting (correlation/auth/tracing) converging on GraphQLContext, isolate web-only I/O, and account for context-propagation and instrumentation-vs-interceptor layering across transports.

**The interception layering.** Spring for GraphQL deliberately provides *two* interceptor SPIs, aligned to transports: - **`WebGraphQlInterceptor`** — for the **Web** transports, which means both **HTTP** and **WebSocket** (both are modeled as `WebGraphQlRequest`/`WebGraphQlResponse`). One interceptor bean thus applies to both. - **`RSocketGraphQlInterceptor`** — for the **RSocket** transport, with its own request/response types. Both expose the same shape: `intercept(request, chain)` returning a reactive result, with a `Chain.next(...)` to continue. They wrap graphql-java execution as a **nested chain of filters**. **Assembling and ordering the chain.** Every `WebGraphQlInterceptor`/`RSocketGraphQlInterceptor` **bean** is auto-detected and composed into a chain. Order is controlled by implementing **`Ordered`** or annotating **`@Order`**: **lower value = earlier = outermost**. The outermost interceptor sees the request **first** (before inner ones) and the response **last** (after inner ones complete) — classic onion/filter semantics. This matters when, say, a tracing interceptor must wrap timing around everything, so it should be outermost (lowest order). **Where cross-cutting state lives: the GraphQLContext.** The clean way to make logic transport-agnostic is to funnel shared values into the per-execution **`GraphQLContext`** (a key/value map graphql-java threads through every fetcher). A correlation ID generated in an HTTP interceptor and one generated in an RSocket interceptor both land in the same place, so **fetchers and instrumentation don't care which transport delivered the request**. Read them downstream with `@ContextValue` or `DataFetchingEnvironment#getGraphQlContext()`. **Transport-specific asymmetries (the real trade-offs):** 1. **HTTP response headers / cookies** exist on `WebGraphQlResponse` (e.g. `response.getResponseHeaders().add(...)`) but have **no RSocket equivalent** — RSocket has metadata, not HTTP headers. So "add an `X-Trace-Id` response header" is a web-only capability; over RSocket you'd convey it via payload/metadata or simply not at all. 2. **WebSocket header timing.** Over WebSocket, HTTP headers (auth token, tenant) are available only at the **handshake/`connection_init`**, not on subsequent `subscribe` messages. A `WebGraphQlInterceptor` that reads `request.getHeaders()` per operation works for plain HTTP but yields nothing useful mid-WebSocket-connection. Design auth/tenant resolution to happen **once at connection establishment** and be cached in the connection's context. 3. **Reactive vs blocking.** Interceptors are reactive (`Mono`), uniform across transports, but the fetchers behind them may be blocking (MVC) or reactive (WebFlux/RSocket). Context propagation (Micrometer context-propagation + `ThreadLocalAccessor`s) is what keeps `SecurityContext`/MDC coherent across those boundaries — so cross-cutting security/logging context set in an interceptor survives to off-thread fetchers. **A pragmatic pattern.** Extract the transport-neutral behavior (generate/propagate correlation ID, resolve tenant, stash auth context) into a small shared component that writes into the `GraphQLContext`, and have thin `WebGraphQlInterceptor` and `RSocketGraphQlInterceptor` beans call it plus do their transport-specific I/O (e.g., web interceptor also copies the correlation ID onto the HTTP response header). This keeps one source of truth and avoids drift between transports. **When this matters.** Multi-transport GraphQL (browser via WebSocket, internal services via RSocket, request/response via HTTP) in an org that wants uniform observability, tracing, and authz. For single-transport apps, one `WebGraphQlInterceptor` usually suffices and the RSocket concerns are moot. **Gotchas.** - Don't assume an interceptor runs per field — it runs **per operation/request**; per-field cross-cutting belongs in graphql-java `Instrumentation` or `@SchemaMapping` logic. - Beware ordering surprises: two interceptors both calling `configureExecutionInput` compose, but response mutations happen in reverse (innermost first) — reason about the onion. - Over-centralizing web-specific response-header logic into "shared" code breaks on RSocket; keep the asymmetric bits in the transport-specific interceptor.

  • Why can't you set an HTTP response header from an RSocketGraphQlInterceptor?
    RSocket has no HTTP response header concept — it uses frames and binary metadata, not an HttpHeaders response object. Response headers are a Web transport feature exposed on WebGraphQlResponse; over RSocket you'd convey such data via payload or RSocket metadata instead.
  • Two WebGraphQlInterceptors both post-process the response. Which one's mutation happens first?
    The innermost (highest @Order value) runs its response logic first, because the response bubbles outward: outermost sees the request first but the response last. So order determines both request-in and response-out sequencing in mirror image.
  • You need per-field timing across transports. Is a WebGraphQlInterceptor the right tool?
    No — interceptors run per operation, not per field. Per-field cross-cutting belongs in a graphql-java Instrumentation (e.g., instrumentDataFetcher), which the interceptor can complement by seeding the GraphQLContext with a correlation ID the instrumentation reads.

saying these in an interview costs you the question

  • Assuming one interceptor type covers all three transports (RSocket needs RSocketGraphQlInterceptor)
  • Expecting HTTP response headers to work over RSocket
  • Reading WebSocket per-message headers after the connection is open
  • Thinking @Order higher value runs first/outermost (it's the opposite)
  • Using an interceptor for per-field concerns instead of graphql-java Instrumentation

context