[[client]] = Client Spring for GraphQL includes client support to execute GraphQL requests over HTTP, WebSocket, and RSocket. [[client.graphqlclient]] == `GraphQlClient` `GraphQlClient` defines a common workflow for GraphQL requests independent of the underlying transport, so the way you perform requests is the same no matter what transport is in use. The following transport specific `GraphQlClient` extensions are available: - xref:client.adoc#client.httpsyncgraphqlclient[HttpSyncGraphQlClient] - xref:client.adoc#client.httpgraphqlclient[HttpGraphQlClient] - xref:client.adoc#client.websocketgraphqlclient[WebSocketGraphQlClient] - xref:client.adoc#client.rsocketgraphqlclient[RSocketGraphQlClient] Each defines a `Builder` with options relevant to the transport. All builders extend from a common, base GraphQlClient xref:client.adoc#client.graphqlclient.builder[`Builder`] with options applicable to all transports. Once `GraphQlClient` is built you can begin to make xref:client.adoc#client.requests[requests]. [[client.httpsyncgraphqlclient]] === HTTP Sync `HttpSyncGraphQlClient` uses {spring-framework-ref-docs}/integration/rest-clients.html#rest-restclient[RestClient] to execute GraphQL requests over HTTP through a blocking transport contract and chain of interceptors. [source,java,indent=0,subs="verbatim,quotes"] ---- RestClient restClient = ... ; HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient); ---- Once `HttpSyncGraphQlClient` is created, you can begin to xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying transport. If you need to change any transport specific details, use `mutate()` on an existing `HttpSyncGraphQlClient` to create a new instance with customized settings: [source,java,indent=0,subs="verbatim,quotes"] ---- RestClient restClient = ... ; HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.builder(restClient) .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Perform requests with graphQlClient... HttpSyncGraphQlClient anotherGraphQlClient = graphQlClient.mutate() .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Perform requests with anotherGraphQlClient... ---- [[client.httpgraphqlclient]] === HTTP `HttpGraphQlClient` uses {spring-framework-ref-docs}/web/webflux-webclient.html[WebClient] to execute GraphQL requests over HTTP through a non-blocking transport contract and chain of interceptors. [source,java,indent=0,subs="verbatim,quotes"] ---- WebClient webClient = ... ; HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient); ---- Once `HttpGraphQlClient` is created, you can begin to xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying transport. If you need to change any transport specific details, use `mutate()` on an existing `HttpGraphQlClient` to create a new instance with customized settings: [source,java,indent=0,subs="verbatim,quotes"] ---- WebClient webClient = ... ; HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient) .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Perform requests with graphQlClient... HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate() .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Perform requests with anotherGraphQlClient... ---- [[client.websocketgraphqlclient]] === WebSocket `WebSocketGraphQlClient` executes GraphQL requests over a shared WebSocket connection. It is built using the {spring-framework-ref-docs}/web/webflux-websocket.html#webflux-websocket-client[WebSocketClient] from Spring WebFlux and you can create it as follows: [source,java,indent=0,subs="verbatim,quotes"] ---- String url = "wss://localhost:8080/graphql"; WebSocketClient client = new ReactorNettyWebSocketClient(); WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build(); ---- In contrast to `HttpGraphQlClient`, the `WebSocketGraphQlClient` is connection oriented, which means it needs to establish a connection before making any requests. As you begin to make requests, the connection is established transparently. Alternatively, use the client's `start()` method to establish the connection explicitly before any requests. In addition to being connection-oriented, `WebSocketGraphQlClient` is also multiplexed. It maintains a single, shared connection for all requests. If the connection is lost, it is re-established on the next request or if `start()` is called again. You can also use the client's `stop()` method which cancels in-progress requests, closes the connection, and rejects new requests. TIP: Use a single `WebSocketGraphQlClient` instance for each server in order to have a single, shared connection for all requests to that server. Each client instance establishes its own connection and that is typically not the intent for a single server. Once `WebSocketGraphQlClient` is created, you can begin to xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying transport. If you need to change any transport specific details, use `mutate()` on an existing `WebSocketGraphQlClient` to create a new instance with customized settings: [source,java,indent=0,subs="verbatim,quotes"] ---- URI url = ... ; WebSocketClient client = ... ; WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client) .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Use graphQlClient... WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate() .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Use anotherGraphQlClient... ---- [[client.websocketgraphqlclient.interceptor]] ==== Interceptor The https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md[GraphQL over WebSocket] protocol defines a number of connection oriented messages in addition to executing requests. For example, a client sends `"connection_init"` and the server responds with `"connection_ack"` at the start of a connection. For WebSocket transport specific interception, you can create a `WebSocketGraphQlClientInterceptor`: [source,java,indent=0,subs="verbatim,quotes"] ---- static class MyInterceptor implements WebSocketGraphQlClientInterceptor { @Override public Mono connectionInitPayload() { // ... the "connection_init" payload to send } @Override public Mono handleConnectionAck(Map ackPayload) { // ... the "connection_ack" payload received } } ---- xref:client.adoc#client.interception[Register] the above interceptor as any other `GraphQlClientInterceptor` and use it also to intercept GraphQL requests, but note there can be at most one interceptor of type `WebSocketGraphQlClientInterceptor`. [[client.rsocketgraphqlclient]] === RSocket `RSocketGraphQlClient` uses {spring-framework-ref-docs}/rsocket.html#rsocket-requester[RSocketRequester] to execute GraphQL requests over RSocket requests. [source,java,indent=0,subs="verbatim,quotes"] ---- URI uri = URI.create("wss://localhost:8080/rsocket"); WebsocketClientTransport transport = WebsocketClientTransport.create(url); RSocketGraphQlClient client = RSocketGraphQlClient.builder() .clientTransport(transport) .build(); ---- In contrast to `HttpGraphQlClient`, the `RSocketGraphQlClient` is connection oriented, which means it needs to establish a session before making any requests. As you begin to make requests, the session is established transparently. Alternatively, use the client's `start()` method to establish the session explicitly before any requests. `RSocketGraphQlClient` is also multiplexed. It maintains a single, shared session for all requests. If the session is lost, it is re-established on the next request or if `start()` is called again. You can also use the client's `stop()` method which cancels in-progress requests, closes the session, and rejects new requests. TIP: Use a single `RSocketGraphQlClient` instance for each server in order to have a single, shared session for all requests to that server. Each client instance establishes its own connection and that is typically not the intent for a single server. Once `RSocketGraphQlClient` is created, you can begin to xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying transport. [[client.graphqlclient.builder]] === Builder `GraphQlClient` defines a parent `BaseBuilder` with common configuration options for the builders of all extensions. Currently, it has lets you configure: - `DocumentSource` strategy to load the document for a request from a file - xref:client.adoc#client.interception[Interception] of executed requests `BaseBuilder` is further extended by the following: - `SyncBuilder` - blocking execution stack with a chain of ``SyncGraphQlInterceptor``'s. - `Builder` - non-blocking execution stack with chain of ``GraphQlInterceptor``'s. [[client.requests]] == Requests Once you have a xref:client.adoc#client.graphqlclient[`GraphQlClient`], you can begin to perform requests via xref:client.adoc#client.requests.retrieve[retrieve] or xref:client.adoc#client.requests.execute[execute] methods. [[client.requests.retrieve]] === Retrieve The below retrieves and decodes the data for a query: [tabs] ====== Sync:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- String document = "{" + " project(slug:\"spring-framework\") {" + " name" + " releases {" + " version" + " }"+ " }" + "}"; Project project = graphQlClient.document(document) <1> .retrieveSync("project") <2> .toEntity(Project.class); <3> ---- Non-Blocking:: + [source,java,indent=0,subs="verbatim,quotes",role="secondary"] ---- String document = "{" + " project(slug:\"spring-framework\") {" + " name" + " releases {" + " version" + " }"+ " }" + "}"; Mono projectMono = graphQlClient.document(document) <1> .retrieve("project") <2> .toEntity(Project.class); <3> ---- ====== <1> The operation to perform. <2> The path under the "data" key in the response map to decode from. <3> Decode the data at the path to the target type. The input document is a `String` that could be a literal or produced through a code generated request object. You can also define documents in files and use a xref:client.adoc#client.requests.document-source[Document Source] to resole them by file name. The path is relative to the "data" key and uses a simple dot (".") separated notation for nested fields with optional array indices for list elements, e.g. `"project.name"` or `"project.releases[0].version"`. Decoding can result in `FieldAccessException` if the given path is not present, or the field value is `null` and has an error. `FieldAccessException` provides access to the response and the field: [tabs] ====== Sync:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- try { Project project = graphQlClient.document(document) .retrieveSync("project") .toEntity(Project.class); } catch (FieldAccessException ex) { ClientGraphQlResponse response = ex.getResponse(); // ... ClientResponseField field = ex.getField(); // ... } ---- Non-Blocking:: + [source,java,indent=0,subs="verbatim,quotes",role="secondary"] ---- Mono projectMono = graphQlClient.document(document) .retrieve("project") .toEntity(Project.class) .onErrorResume(FieldAccessException.class, ex -> { ClientGraphQlResponse response = ex.getResponse(); // ... ClientResponseField field = ex.getField(); // ... }); ---- ====== [[client.requests.execute]] === Execute xref:client.adoc#client.requests.retrieve[Retrieve] is only a shortcut to decode from a single path in the response map. For more control, use the `execute` method and handle the response: For example: [tabs] ====== Sync:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- ClientGraphQlResponse response = graphQlClient.document(document).executeSync(); if (!response.isValid()) { // Request failure... <1> } ClientResponseField field = response.field("project"); if (!field.hasValue()) { if (field.getError() != null) { // Field failure... <2> } else { // Optional field set to null... <3> } } Project project = field.toEntity(Project.class); <4> ---- Non-Blocking:: + [source,java,indent=0,subs="verbatim,quotes",role="secondary"] ---- Mono projectMono = graphQlClient.document(document) .execute() .map(response -> { if (!response.isValid()) { // Request failure... <1> } ClientResponseField field = response.field("project"); if (!field.hasValue()) { if (field.getError() != null) { // Field failure... <2> } else { // Optional field set to null... <3> } } return field.toEntity(Project.class); <4> }); ---- ====== <1> The response does not have data, only errors <2> Field that is `null` and has an associated error <3> Field that was set to `null` by its `DataFetcher` <4> Decode the data at the given path [[client.requests.document-source]] === Document Source The document for a request is a `String` that may be defined in a local variable or constant, or it may be produced through a code generated request object. You can also create document files with extensions `.graphql` or `.gql` under `"graphql-documents/"` on the classpath and refer to them by file name. For example, given a file called `projectReleases.graphql` in `src/main/resources/graphql-documents`, with content: [source,graphql,indent=0,subs="verbatim,quotes"] .src/main/resources/graphql-documents/projectReleases.graphql ---- query projectReleases($slug: ID!) { project(slug: $slug) { name releases { version } } } ---- You can then: [source,java,indent=0,subs="verbatim,quotes"] ---- Project project = graphQlClient.documentName("projectReleases") <1> .variable("slug", "spring-framework") <2> .retrieveSync() .toEntity(Project.class); ---- <1> Load the document from "projectReleases.graphql" <2> Provide variable values. The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion. You can use the `GraphQlClient` xref:client.adoc#client.graphqlclient.builder[Builder] to customize the `DocumentSource` for loading documents by names. [[client.subscriptions]] == Subscription Requests `GraphQlClient` can execute subscriptions over transports that support it. Only the WebSocket and RSocket transports support GraphQL subscriptions, so you'll need to create a xref:client.adoc#client.websocketgraphqlclient[WebSocketGraphQlClient] or xref:client.adoc#client.rsocketgraphqlclient[RSocketGraphQlClient]. [[client.subscriptions.retrieve]] === Retrieve To start a subscription stream, use `retrieveSubscription` which is similar to xref:client.adoc#client.requests.retrieve[retrieve] for a single response but returning a stream of responses, each decoded to some data: [source,java,indent=0,subs="verbatim,quotes"] ---- Flux greetingFlux = client.document("subscription { greetings }") .retrieveSubscription("greeting") .toEntity(String.class); ---- The `Flux` may terminate with `SubscriptionErrorException` if the subscription ends from the server side with an "error" message. The exception provides access to GraphQL errors decoded from the "error" message. The `Flux` may termiate with `GraphQlTransportException` such as `WebSocketDisconnectedException` if the underlying connection is closed or lost. In that case you can use the `retry` operator to restart the subscription. To end the subscription from the client side, the `Flux` must be cancelled, and in turn the WebSocket transport sends a "complete" message to the server. How to cancel the `Flux` depends on how it is used. Some operators such as `take` or `timeout` themselves cancel the `Flux`. If you subscribe to the `Flux` with a `Subscriber`, you can get a reference to the `Subscription` and cancel through it. The `onSubscribe` operator also provides access to the `Subscription`. [[client.subscriptions.execute]] === Execute xref:client.adoc#client.subscriptions.retrieve[Retrieve] is only a shortcut to decode from a single path in each response map. For more control, use the `executeSubscription` method and handle each response directly: [source,java,indent=0,subs="verbatim,quotes"] ---- Flux greetingFlux = client.document("subscription { greetings }") .executeSubscription() .map(response -> { if (!response.isValid()) { // Request failure... } ClientResponseField field = response.field("project"); if (!field.hasValue()) { if (field.getError() != null) { // Field failure... } else { // Optional field set to null... <3> } } return field.toEntity(String.class) }); ---- [[client.interception]] == Interception For blocking transports created with the `GraphQlClient.SyncBuilder`, you create a `SyncGraphQlClientInterceptor` to intercept all requests through the client: [source,java,indent=0,subs="verbatim,quotes"] ---- static class MyInterceptor implements SyncGraphQlClientInterceptor { @Override public ClientGraphQlResponse intercept(ClientGraphQlRequest request, Chain chain) { // ... return chain.next(request); } } ---- For non-blocking transports created with `GraphQlClient.Builder`, you create a `GraphQlClientInterceptor` to intercept all requests through the client: [source,java,indent=0,subs="verbatim,quotes"] ---- static class MyInterceptor implements GraphQlClientInterceptor { @Override public Mono intercept(ClientGraphQlRequest request, Chain chain) { // ... return chain.next(request); } @Override public Flux interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) { // ... return chain.next(request); } } ---- Once the interceptor is created, register it through the client builder. For example: [source,java,indent=0,subs="verbatim,quotes"] ---- URI url = ... ; WebSocketClient client = ... ; WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client) .interceptor(new MyInterceptor()) .build(); ----