graphql-sse

repository·master·Indexed 19 days ago

https://github.com/enisdenjo/graphql-sse

A zero-dependency, HTTP/1 safe implementation of the GraphQL over Server-Sent Events (SSE) protocol. It provides both a server handler and a client to support GraphQL subscriptions and queries as an alternative to WebSockets. The library supports two operation modes: Distinct Connections Mode for HTTP/2+ servers and Single Connection Mode (using a reservation token system) to bypass browser connection limits on HTTP/1 servers.

Tokens
17.8K
Snippets
55
Records
67
Agent score
65%

What's inside graphql-sse

  1. Overview of graphql-sse

    master
    graphql-sse is a zero-dependency server and client implementation for GraphQL over Server-Sent Events (SSE). It is designed to be simple and HTTP/1 safe. If your project requires WebSockets instead of SSE, you should use graphql-ws instead.
  2. Use Distinct Connections Mode

    master

    In this mode, every operation request is its own SSE stream.

    Requirements:

    • Content-Type must be text/event-stream.
    • Validation Errors: Do not return a 400 status code for validation errors, as this will cause the browser's EventSource to terminate the connection. Instead, emit a next event containing the errors in the data field.
    • Termination: To stop streaming operations (like subscriptions, @stream, or @defer), the client simply closes the SSE connection.

    Event Stream Format:

    • next event: Contains the ExecutionResult.
    • complete event: Indicates the operation is finished. Note: When using the browser's native EventSource, you MUST include an empty data: field in the complete event, otherwise the listener may not trigger.
    // next event structure
    interface NextMessage {
      event: 'next';
      data: ExecutionResult;
    }
    
    // complete event structure
    interface CompleteMessage {
      event: 'complete';
    }
  3. Understand the two modes of GraphQL over SSE

    master

    The graphql-sse protocol supports two distinct modes of operation depending on your server's HTTP capabilities and connection limits:

    1. Distinct Connections Mode: Best for HTTP/2+ powered servers. Each GraphQL operation request establishes its own dedicated SSE connection. This mode follows the standard GraphQL over HTTP spec but requires Content-Type: text/event-stream and mandates that validation errors be sent as next events rather than 400 Bad Request to prevent the client's EventSource from failing.

    2. Single Connection Mode: Designed to bypass the browser's connection limit (typically 6 connections per domain) on HTTP/1 servers. In this mode, a single long-lived SSE connection is used to stream results for all GraphQL operations. Separate HTTP requests are used to trigger operations, and a "reservation" system is used to link these requests to the single stream.

  4. Implement Persisted Queries

    master

    To implement Persisted Queries, use the onSubscribe hook on the server to look up the query in a store using an ID provided in the client's extensions.persistedQuery field. If found, return the stored ExecutionArgs. On the client, you can leave the query field empty and provide the ID in the extensions.

    Server Implementation: Use onSubscribe to match params.extensions?.persistedQuery against your store.

    Client Implementation: Pass the ID in the extensions object of the subscription request.

    // Server
    export const handler = createHandler({
      onSubscribe: (_req, params) => {
        const persistedQuery = queriesStore[String(params.extensions?.persistedQuery)];
        if (persistedQuery) {
          return {
            ...persistedQuery,
            variableValues: params.variables,
            contextValue: undefined,
          };
        }
        return [null, { status: 404, statusText: 'Not Found' }];
      },
    });
    
    // Client
    client.subscribe(
      {
        query: '',
        extensions: {
          persistedQuery: 'iWantTheGreetings',
        },
      },
      {
        next: onNext,
        error: reject,
        complete: resolve,
      },
    );
  5. Start a GraphQL SSE server with Node.js http2

    master

    Use createHandler from graphql-sse/lib/use/http2 for HTTP/2 servers. Note that browsers may require SSL/TLS certificates for HTTP/2. You can generate self-signed certificates using openssl.

    $ openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
      -keyout localhost-privkey.pem -out localhost-cert.pem
    import http2 from 'http2';
    import fs from 'fs';
    import { createHandler } from 'graphql-sse/lib/use/http2';
    import { schema } from './previous-step';
    
    const handler = createHandler({ schema });
    
    const server = http2.createSecureServer(
      {
        key: fs.readFileSync('localhost-privkey.pem'),
        cert: fs.readFileSync('localhost-cert.pem'),
      },
      (req, res) => {
        if (req.url.startsWith('/graphql/stream')) {
          return handler(req, res);
        }
        return res.writeHead(404).end();
      },
    );
    
    server.listen(4000);
  6. Start a GraphQL SSE server with Node.js http

    master

    Use createHandler from graphql-sse/lib/use/http to create a handler for standard Node.js http requests.

    import http from 'http';
    import { createHandler } from 'graphql-sse/lib/use/http';
    import { schema } from './previous-step';
    
    const handler = createHandler({ schema });
    
    const server = http.createServer((req, res) => {
      if (req.url.startsWith('/graphql/stream')) {
        return handler(req, res);
      }
      res.writeHead(404).end();
    });
    
    server.listen(4000);
  7. Start a GraphQL SSE server with Koa

    master

    Use createHandler from graphql-sse/lib/use/koa and koa-mount to integrate with a Koa application.

    import Koa from 'koa';
    import mount from 'koa-mount';
    import { createHandler } from 'graphql-sse/lib/use/koa';
    import { schema } from './previous-step';
    
    const app = new Koa();
    
    app.use(mount('/graphql/stream', createHandler({ schema })));
    
    app.listen({ port: 4000 });
  8. Handle thrown errors in the subscribe method

    master

    Because graphql-js does not catch errors thrown from async iterables, errors in a subscription can bubble up to the server handler and cause issues like attempting to write headers after they have been sent. To prevent this, you can override the subscribe method in createHandler to wrap the async iterator's next method in a try/catch block, converting thrown errors into GraphQL error objects.

    import { subscribe } from 'graphql';
    import { createHandler } from 'graphql-sse';
    
    export const handler = createHandler({
      async subscribe(...args) {
        const result = await subscribe(...args);
        if ('next' in result) {
          const originalNext = result.next;
          result.next = () =>
            originalNext().catch((err) => ({ value: { errors: [err] } }));
        }
        return result;
      },
    });
  9. Start a GraphQL SSE server with Express

    master

    Use createHandler from graphql-sse/lib/use/express to integrate with an Express application.

    import express from 'express';
    import { createHandler } from 'graphql-sse/lib/use/express';
    import { schema } from './previous-step';
    
    const handler = createHandler({ schema });
    const app = express();
    
    app.use('/graphql/stream', handler);
    
    app.listen(4000);
  10. Start a GraphQL SSE server with Fastify

    master

    Use createHandler from graphql-sse/lib/use/fastify to integrate with a Fastify application.

    import Fastify from 'fastify';
    import { createHandler } from 'graphql-sse/lib/use/fastify';
    import { schema } from './previous-step';
    
    const handler = createHandler({ schema });
    const fastify = Fastify();
    
    fastify.all('/graphql/stream', handler);
    
    fastify.listen({ port: 4000 });
  11. Implement custom authentication in the server handler

    master

    When using createHandler, you can provide an authenticate function to handle user authorization.

    There are two modes of operation to consider:

    1. Distinct Connections Mode: Clients request an event-stream using a POST method. For these requests, you can simply return an empty string '' (or the token) to indicate the client is authenticated.
    2. Single Connection Mode: Clients use a single connection for multiple operations. These subsequent requests will include an X-GraphQL-Event-Stream-Token header. You must extract this token from the header to identify the stream. If the token is missing for a single-connection request, you must generate or retrieve a unique token (e.g., from cookies or an Authorization header).

    If authentication fails, you can return a tuple containing null and a response object with a status code, such as [null, { status: 401, statusText: 'Unauthorized' }].

    import { createHandler } from 'graphql-sse';
    
    export const handler = createHandler({
      schema,
      authenticate: async (req) => {
        let token = req.headers.get('x-graphql-event-stream-token');
        if (token) {
          // Handle Single Connection Mode
          return Array.isArray(token) ? token.join('') : token;
        }
    
        // Logic to find/create token for new connections
        token = await getOrCreateTokenFromCookies(req);
    
        if (!token) {
          return [null, { status: 401, statusText: 'Unauthorized' }];
        }
    
        // Handle Distinct Connections Mode
        if (req.method === 'POST' && req.headers.get('accept') === 'text/event-stream') {
          return '';
        }
    
        return token;
      },
    });