There are 2 currently available mechanisms for delivering partial responses from GraphQL Server: multipart/mixed responses and SSE. Both are HTTP standards and are widely adopted. Let’s start with multipart/mixed content type responses.
- The typical multipart/mixed response looks like:
---
Content-Type: application/json; charset=utf-8
Content-Length: 93
{"data":{"alphabet":["a","b","c","d","e","f","g"],"fastField":"Initial data"},"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 78
{"incremental":[{"data":{"slowField":"I am slow"},"path":[]}],"hasNext":false}
-----
Pay attention that in this case the response headers are lack the usual ‘Content-Length’ or ‘Transfer-Encoding’. If they were present the data received on the socked was indicated the end of the response body. (The client usually buffers the retrieved data to allow the underlying TCP connection to go idle).
But we want the opposite, we want response body be interpreted as a stream. The whole point of such a response is that it isn’t delivered at once. Whenever some part (section) of the response is ready at the server side, it is written to the HTTP response stream that is kept open from the initial point. On the other hand, the response stream made available to the client from the very beginning, so those parts which were written to the stream became available as soon as they were transferred to the client by the underlying connection.
Apollo JS (React) Client for GraphQL supports such a responses with minimal configuration:
// ========== src/apollo/client.js ==========
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
headers: { 'accept': 'multipart/mixed' }
}),
cache: new InMemoryCache()
});
export default client;
Here the endpoint (…/graphql) should be changed. Once configured, this client is passed to ApolloProvider:
// ========== src/App.js ==========
import React, {useState, useEffect} from "react";
import { ApolloProvider } from '@apollo/client';
import client from './apollo/client';
import ProductDetails from './components/ProductDetails.jsx';
const App = () => {
return(<ApolloProvider client={client}>
<ProductDetails />
</ApolloProvider>)
}
export default App;
And now the funny thing. The client uses the query with deferred fragment (decorated with @defer directive). In our case the fragment inside the query gets the argument - waitFor
import { gql } from '@apollo/client';
export const MY_QUERY = gql`
query MyQuery ($waitFor: Int){
alphabet
... on Query @defer {
slowField(waitFor: $waitFor)
}
fastField
}
`;
Finally, Apollo provides useQuery() hook to retrieve the data from http response. Being chucked , the data’s are loaded incrementally
import React from "react";
import { MY_QUERY } from '../graphql/queries';
import { useQuery } from '@apollo/client';
const ProductDetails = () => {
const {loading, data, error} = useQuery(MY_QUERY, { variables: {waitFor: 6000} });
if (loading) return <p>Loading ...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>
<h1>{data.fastField}</h1>
{data.slowField ? (
<div>data</div>
) :
(<p>Loading slow field...</p>)
}
</div>
}
export default ProductDetails;
- The server part of the story does requires a little configuration to be aware of the @defer directive. For example, Yoga GraphQL server supports it by useDeferStream plugin
import { createSchema, createYoga } from 'graphql-yoga'
import { useDeferStream } from '@graphql-yoga/plugin-defer-stream'
//....
const typeDefs = /* GraphQL */ `
# ....
type Query {
me: User
}
`;
const resolvers = {
Query: {
...
}
}
const yoga = createYoga({
schema: createSchema({
typeDefs,
resolvers
}),
plugins: [useDeferStream()]
})
This “small” plugin basically changes the underlying infrastructure of the server’s response part. With this plugin, the multipart/mixed content-type responses are returned if @defer directive is properly set on the queries. Without this directive, the response is ‘application/json’ content typed.
Note that aside from the mentioned plugin, the resolvers and other parts of the server remain untouched by the @desc directive. Just the same resolvers will be executed in both cases, but in different contexts.
Reading HTTP stream from C
var obj = new
{
// operationName = "MyQuery",
query = new string("query MyQuery { fastField ... on Query @defer { slowField(waitFor: 5000)}}")
};
JsonContent content = JsonContent.Create(obj);
using var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:4000/graphql");
request.Content = content;
request.Headers.Add("Accept", "multipart/mixed");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
var contentType = response.Content.Headers.ContentType;
var boundary = GetBoundary(contentType);
var multipartReader = new MultipartReader(boundary,
response.Content.ReadAsStream());
while (await multipartReader.ReadNextSectionAsync() is { } section)
{
StreamReader stmReader = new (section.Body);
string strBufSize = section.Headers["Content-Length"];
int bufSize = int.Parse(strBufSize);
char[] buffer = new char[bufSize];
stmReader.Read(buffer, 0, buffer.Length);
Console.WriteLine(buffer);
}
The key point here is HttpCompletionOption.ResponseHeadersRead options flag that states that SendAsync() function will return immediately when the response headers are ready at the client side, and we can inspect these headers. The body of the response may not be fully received at this point, but it is available as a stream. This situation allows us to begin working with the stream of data as soon as portions of the stream become available.
Pay attention also to the disposal of HttpResponseMessage. When using HttpCompletionOption.ResponseHeadersRead we accept more responsibility around system resources, since the connection to the remote server is tied up until we decide that we’re done with the content. The way we signal that is by disposing of the HttpResponseMessage, which then frees up the connection to be used for other resources.
Reading HTTP stream from Swift
Swift’s Foundation has built-in support for reading data from HTTP body incrementally as it arrives with the help of URLSession.streamTask
import Foundation
class StreamedResponseHandler: NSObject, URLSessionDataDelegate {
func fetchStreamedData() {
let url = URL(string: "https://example.com/streaming-endpoint")!
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: URLRequest(url: url))
task.resume()
}
// Called when the server responds with headers
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
print("Headers received: \(response)")
completionHandler(.allow) // Start receiving data
}
// Called whenever a chunk of data is received
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if let text = String(data: data, encoding: .utf8) {
print("Received chunk: \(text)")
}
}
// Called when the response completes
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Error: \(error.localizedDescription)")
} else {
print("Streaming complete.")
}
}
}
let handler = StreamedResponseHandler()
handler.fetchStreamedData()
ccc