|
This version is still in development and is not considered stable yet. For the latest stable version, please use Spring GraphQL 2.0.4! |
Server Transports
Spring for GraphQL supports handling of GraphQL requests over HTTP, WebSocket, and RSocket.
HTTP
GraphQlHttpHandler handles GraphQL over HTTP requests and delegates to the
Interception chain for request execution. There are two variants, one for
Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have equivalent functionality,
but rely on blocking vs non-blocking I/O respectively for writing the HTTP response.
By default, GraphQlHttpHandler only accepts HTTP POST requests, with "application/json" as content
type and GraphQL request details included as JSON in the request body. Clients can request the
"application/graphql-response+json" media type to get the behavior defined in the official
GraphQL over HTTP specification.
If the client doesn’t express any preference, this will be the content type of choice.
Clients can also request the legacy "application/json" media type to get the legacy HTTP behavior.
In practice, GraphQL HTTP clients should expect 4xx/5xx HTTP responses if the server is unavailable, security credentials
are missing or if the request body is not valid JSON. "application/graphql-response+json" responses will also use
4xx statuses if the GraphQL document sent by the client cannot be parsed or is considered invalid by the GraphQL engine.
In this case, "application/json" responses will still use 200 (OK).
Once the GraphQL request has been successfully validated, the HTTP response status is always 200 (OK),
and any errors from GraphQL request execution appear in the "errors" section of the GraphQL response.
GraphQlHttpHandler can be exposed as an HTTP endpoint by declaring a RouterFunction
bean and using the RouterFunctions from Spring MVC or WebFlux to create the route. The
Boot Starter does this, see the
Web Endpoints section for
details, or check GraphQlWebMvcAutoConfiguration or GraphQlWebFluxAutoConfiguration
it contains, for the actual config.
By default, the GraphQlHttpHandler will serialize and deserialize JSON payloads using the HttpMessageConverter (Spring MVC)
and the DecoderHttpMessageReader/EncoderHttpMessageWriter (WebFlux) configured in the web framework.
In some cases, the application will configure the JSON codec for the HTTP endpoint in a way that is not compatible with the GraphQL payloads.
Applications can instantiate GraphQlHttpHandler with a custom JSON codec that will be used for GraphQL payloads.
HTTP GET
GraphQlHttpHandler can optionally also accept HTTP GET requests, as defined in the
GraphQL over HTTP specification. This is
useful for clients that can only issue GET requests, such as a browser EventSource, or for responses
that a CDN or intermediate cache can store. GET support is disabled by default and must be enabled
explicitly, on both the handler and the matching RequestPredicate:
GraphQlHttpHandler httpHandler = GraphQlHttpHandler.builder(webGraphQlHandler)
.httpMethods(HttpMethod.GET, HttpMethod.POST)
.build();
RequestPredicate predicate = GraphQlRequestPredicates.graphQlHttp("/graphql",
Set.of(HttpMethod.GET, HttpMethod.POST));
On a GET request, there is no request body: query and operationName are read as plain query string
parameters, while variables and extensions, when present, must each be a JSON string. An empty
variables or extensions parameter is treated the same as if it were absent. If the request URI
would become too large to encode a request this way, clients should fall back to HTTP POST.
Because GET is a "safe" HTTP method, it must not be used to execute mutations. A GET request whose
operation is a mutation is rejected with a 405 (Method Not Allowed) response, with an Allow header
listing the HTTP methods the endpoint accepts; this applies regardless of the requested media type.
Before enabling GET support, consider the following:
-
The query string, including
variables, typically ends up in access logs, browser history,Refererheaders, and intermediate proxies or caches. Do not enable GET ifvariablesmay carry sensitive data, unless those exposure paths are otherwise mitigated. -
A GET request without a
Content-Typeheader is a CORS "simple request": it is sent cross-origin with credentials and without a preflight check. The response itself is not readable by the calling script cross-origin, but the request is still executed on the server, including any side effects a query might have. -
Spring Security’s
CsrfFiltertreats GET as a safe method and does not require a CSRF token for it.
Mutations remain blocked either way, but read-side side effects, such as rate limiting, cost, or audit logging, still apply and should be considered as part of enabling this transport.
Server-Sent Events
GraphQlSseHandler is very similar to the HTTP handler listed above, but this time handling GraphQL requests over HTTP
using the Server-Sent Events protocol. With this transport, clients send HTTP POST requests to the endpoint by default,
with "application/json" as content type and GraphQL request details included as JSON in the request body; the only
difference with the vanilla HTTP variant is that the client must send "text/event-stream" as the "Accept" request
header. The response will be sent as one or more Server-Sent Event(s).
This is also defined in the proposed GraphQL over HTTP specification. Spring for GraphQL only implements the "Distinct connections mode", so applications must consider scalability concerns and whether adopting HTTP/2 as the underlying transport would help.
The main use case for GraphQlSseHandler is an alternative to the
WebSocket transport, receiving a stream of items as a response to a
subscription operation. Other types of operations, like queries and mutations, are not supported here and should be
using the plain JSON over HTTP transport variant.
Like GraphQlHttpHandler, GraphQlSseHandler only accepts HTTP POST requests by default. HTTP GET
can be enabled the same way, on both the handler and the matching RequestPredicate, which is
useful since a browser EventSource, a common client for this transport, can only issue GET
requests:
GraphQlSseHandler sseHandler = GraphQlSseHandler.builder(webGraphQlHandler)
.httpMethods(HttpMethod.GET, HttpMethod.POST)
.build();
RequestPredicate ssePredicate = GraphQlRequestPredicates.graphQlSse("/graphql",
Set.of(HttpMethod.GET, HttpMethod.POST));
See the HTTP GET section above for the query string encoding rules and the security considerations that also apply here.
File Upload
As a protocol GraphQL focuses on the exchange of textual data. This doesn’t include binary data such as images, but there is a separate, informal graphql-multipart-request-spec that allows file uploads with GraphQL over HTTP.
Spring for GraphQL does not support the graphql-multipart-request-spec directly.
While the spec does provide the benefit of a unified GraphQL API, the actual experience has
led to a number of issues, and best practice recommendations have evolved, see
Apollo Server File Upload Best Practices
for a more detailed discussion.
If you would like to use graphql-multipart-request-spec in your application, you can
do so through the library
multipart-spring-graphql.
WebSocket
GraphQlWebSocketHandler handles GraphQL over WebSocket requests based on the
protocol defined in the
graphql-ws library. The main reason to use
GraphQL over WebSocket is subscriptions which allow sending a stream of GraphQL
responses, but it can also be used for regular queries with a single response.
The handler delegates every request to the Interception chain for further
request execution.
|
GraphQL Over WebSocket Protocols
There are two such protocols, one in the subscriptions-transport-ws library and another in the graphql-ws library. The former is not active and succeeded by the latter. Read this blog post for the history. |
There are two variants of GraphQlWebSocketHandler, one for Spring MVC and one for
Spring WebFlux. Both handle requests asynchronously and have equivalent functionality.
The WebFlux handler also uses non-blocking I/O and back pressure to stream messages,
which works well since in GraphQL Java a subscription response is a Reactive Streams
Publisher.
The graphql-ws project lists a number of
recipes for client use.
GraphQlWebSocketHandler can be exposed as a WebSocket endpoint by declaring a
SimpleUrlHandlerMapping bean and using it to map the handler to a URL path. By default,
the Boot Starter does not expose a GraphQL over WebSocket endpoint,
but you can add a property for the endpoint path to enable it. Please, review
Web Endpoints
in the Boot reference documentation, and the list of supported spring.graphql.websocket
properties.
You can also look at GraphQlWebMvcAutoConfiguration or GraphQlWebFluxAutoConfiguration
for the actual Boot autoconfig details.
The 1.0.x branch of this repository contains a WebFlux WebSocket sample application.
WebSocket Resource Management
Unlike HTTP requests, a WebSocket connection stays open for as long as the client keeps it, and a single session can carry any number of concurrent subscriptions over time. Applications should size their deployment accordingly, so that the amount of CPU and memory used at runtime stays within an expected range. This is also a useful safeguard, since it limits how much a single client can affect the rest of the deployment. The following are worth configuring:
-
The maximum number of open server connections, which is a property of the underlying server or connector rather than something Spring for GraphQL configures directly.
-
The maximum size of a buffered WebSocket message. For the Spring MVC variant, this is configured through the Jakarta WebSocket
ServerContainer, for example withsetDefaultMaxTextMessageBufferSize. For the Spring WebFlux variant, the equivalent setting depends on the underlying reactive server. -
The maximum number of concurrent subscriptions a single WebSocket session is allowed to have, configured with the
maxSubscriptionsPerSessionbuilder option onGraphQlWebSocketHandler. By default, there is no limit; once it is reached, the session is closed.
RSocket
GraphQlRSocketHandler handles GraphQL over RSocket requests. Queries and mutations are
expected and handled as an RSocket request-response interaction while subscriptions are
handled as request-stream.
GraphQlRSocketHandler can be used a delegate from an @Controller that is mapped to
the route for GraphQL requests. For example:
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
@Controller
public class GraphQlRSocketController {
private final GraphQlRSocketHandler handler;
GraphQlRSocketController(GraphQlRSocketHandler handler) {
this.handler = handler;
}
@MessageMapping("graphql")
public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
return this.handler.handle(payload);
}
@MessageMapping("graphql")
public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
return this.handler.handleSubscription(payload);
}
}
Interception
Server transports allow intercepting requests before and after the GraphQL Java engine is called to process a request.
WebGraphQlInterceptor
HTTP and WebSocket
transports invoke a chain of 0 or more WebGraphQlInterceptor, followed by an
ExecutionGraphQlService that calls the GraphQL Java engine.
Interceptors allow applications to intercept incoming requests in order to:
-
Check HTTP request details
-
Customize the
graphql.ExecutionInput -
Add HTTP response headers
-
Customize the
graphql.ExecutionResult -
and more
Spring for GraphQL provides a built-in HttpRequestHeaderInterceptor that copies HTTP headers
from the request to the GraphQL context, which then makes them available to data fetchers
such as annotated controllers. For example in a Spring Boot application this may be done
as follows:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.data.method.annotation.ContextValue;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.server.support.HttpRequestHeaderInterceptor;
import org.springframework.stereotype.Controller;
@Configuration
class RequestHeaderInterceptorConfig {
@Bean
public HttpRequestHeaderInterceptor headerInterceptor() { (1)
return HttpRequestHeaderInterceptor.builder().mapHeader("myHeader").build();
}
}
@Controller
class MyContextValueController { (2)
@QueryMapping
Person person(@ContextValue String myHeader) {
...
}
}
| 1 | Create interceptor to copy an HTTP request header value into the GraphQLContext |
| 2 | Data controller method accesses the value |
An interceptor can also access values added to the GraphQLContext by a controller:
import graphql.GraphQLContext;
import reactor.core.publisher.Mono;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Controller;
// Subsequent access from a WebGraphQlInterceptor
class ResponseHeaderInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { (2)
return chain.next(request).doOnNext((response) -> {
String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString());
});
}
}
@Controller
class MyCookieController {
@QueryMapping
Person person(GraphQLContext context) { (1)
context.put("cookieName", "123");
...
}
}
| 1 | Controller adds value to the GraphQLContext |
| 2 | Interceptor uses the value to add an HTTP response header |
WebGraphQlHandler can modify the ExecutionResult, for example, to inspect and modify
request validation errors that are raised before execution begins and which cannot be
handled with a DataFetcherExceptionResolver:
import java.util.List;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import reactor.core.publisher.Mono;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
class RequestErrorInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
return chain.next(request).map((response) -> {
if (response.isValid()) {
return response; (1)
}
List<GraphQLError> errors = response.getErrors().stream() (2)
.map((error) -> {
GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
// ...
return builder.build();
})
.toList();
return response.transform((builder) -> builder.errors(errors).build()); (3)
});
}
}
| 1 | Return the same if ExecutionResult has a "data" key with non-null value |
| 2 | Check and transform the GraphQL errors |
| 3 | Update the ExecutionResult with the modified errors |
Use WebGraphQlHandler to configure the WebGraphQlInterceptor chain. This is supported
by the Boot Starter, see
Web Endpoints.
WebSocketGraphQlInterceptor
WebSocketGraphQlInterceptor extends WebGraphQlInterceptor with additional callbacks
to handle the start and end of a WebSocket connection, in addition to client-side
cancellation of subscriptions. The same also intercepts every GraphQL request on the
WebSocket connection.
Use WebGraphQlHandler to configure the WebGraphQlInterceptor chain. This is supported
by the Boot Starter, see
Web Endpoints.
There can be at most one WebSocketGraphQlInterceptor in a chain of interceptors.
There are two built-in WebSocket interceptors called AuthenticationWebSocketInterceptor,
one for the WebMVC and one for the WebFlux transports. These help to extract authentication
details from the payload of a "connection_init" GraphQL over WebSocket message, authenticate,
and then propagate the SecurityContext to subsequent requests on the WebSocket connection.
| There is a websocket-authentication sample in spring-graphql-examples. |
RSocketQlInterceptor
Similar to WebGraphQlInterceptor, an RSocketQlInterceptor allows intercepting
GraphQL over RSocket requests before and after GraphQL Java engine execution. You can use
this to customize the graphql.ExecutionInput and the graphql.ExecutionResult.