Giter Site home page Giter Site logo

enisdenjo / graphql-sse Goto Github PK

View Code? Open in Web Editor NEW
370.0 7.0 16.0 12.61 MB

Zero-dependency, HTTP/1 safe, simple, GraphQL over Server-Sent Events Protocol server and client.

Home Page: https://the-guild.dev/graphql/sse

License: MIT License

JavaScript 3.01% TypeScript 85.45% MDX 11.54%
graphql client server sse server-sent-events transport observables subscriptions express apollo

graphql-sse's Introduction


graphql-sse

Zero-dependency, HTTP/1 safe, simple, GraphQL over Server-Sent Events Protocol server and client.

Continuous integration graphql-sse

Use WebSockets instead? Check out graphql-ws!


Swiftly start with the get started guide on the website.

Short and concise code snippets for starting with common use-cases. Available on the website.

Auto-generated by TypeDoc and then rendered on the website.

Read about the exact transport intricacies used by the library in the GraphQL over Server-Sent Events Protocol document.

File a bug, contribute with code, or improve documentation? Read up on our guidelines for contributing and drive development with yarn test --watch away!

graphql-sse's People

Contributors

asadhazara avatar dimamachina avatar dotansimha avatar enisdenjo avatar gilgardosh avatar henrinormak avatar michaelstaib avatar santino avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-sse's Issues

Support TypedDocumentNode

It would be great if we're able to pass a TypedDocumentNode query like so:

export const thingSubscription = graphql(/* GraphQL */ `
  subscription DoThing($idOfThing: Int!) {
    queryHere(idOfThing: $idOfThing) {
      id
      test
    }
  }
`);

const client = createClient({
  // singleConnection: true, preferred for HTTP/1 enabled servers and subscription heavy apps
  url: "https://localhost:8080/graphql",
});

const subscription = client.iterate({
  query: thingSubscription,
            // ^ This has a TypeScript error
});

Where /* GraphQL */ is a mechanism to generate TypedNodes via @graphql-codegen/cli.

This would allow TypeScript users to have:

  • Type strict values on the iterable objects
  • Type strict variables, warning you when they're not completed

And more.

Reconnection not working on Chrome

Screenshot
Screen Shot 2022-06-08 at 18 22 17

Expected Behaviour
Disconnecting the client from the internet and the reconnecting should cause it to reconnect when graphql-sse performs a re-connection attempt.

I'm using a mostly standard setup of the graphql-sse client with Relay.

Actual Behaviour
I see an error ending up in the console as in the screenshot. It's not being handled by graphql-sse and reconnecting. The error ends up at the top of my sink.

Firefox and Edge actually work for me. The problem is in Chrome and Safari.

Debug Information
I discovered that the error surfaces in this catch clause in the form of a TypeError with the message: network error (which is standard with the Fetch API AFAIK) but since it was never converted to a NetworkError, it doesn't trigger a retry.

I dug deeper to find the source of the error and see why it wasn't converted to NetworkError and noticed that it was thrown from the toAsyncIterator function, from which apparently you didn't anticipate network errors occurring.

Further Information

  • I tried disabling the QUIQ protocol via a Chrome flag and the console error changes to ERR_NETWORK_CHANGED

Pleas let me know if you aren't able to reproduce and whether I can help with anything.

How to add parameters to With EventSource

Discussed in #73

Originally posted by zahrat August 6, 2023
I want to use graphql-sse in client-side ( with react-native). I read this link but How to pass parameters to query? for example for subscription I have to pass userId , it's something like this:
subscription ($userId: Int!) { notificationAdded(userId: $userId) { text isReaded notificationType senderId id } }
how to do that? I try this but its not working:
const url = new URL( api_url, ); url.searchParams.append( 'query', notificationAddedSubscription ); url.searchParams.append('variables', JSON.stringify({userId: 5}));

Pass all supported options to the fetch function

Hi @enisdenjo ,
some time ago following our conversation you introduced support for credentials option on the fetch function.
Today I am back to ask to introduce support for additional options.

Story

Make sure all supported options are passed to the fetch function.

Acceptance criteria

Currently, the fetch implementation has a whitelist approach to pass options to fetch.
Specifically, only headers, credentials, and body are passed to the underlying fetch function.

However, fetch supports many additional options, such as mode, cache, redirect, referrer, referrerPolicy, integrity, keepalive, and signal (this is actually something you custom implement and so possibly should not be replaced).

Developers might need to specify any of those valid additional options and so these must be passed to fetch.

I have just opened a similar PR, in fetch-multipart-graphql#46, where I proposed dropping the whitelist approach so that all developer-defined options are passed to fetch.
This shifts the responsibility to define valid options to developers as opposed to being forced to maintain a whitelist.
Clearly, you can still override options that need a custom implementation, such as signal.

Let me know your thoughts, it's a simple change, but I am also happy to open a PR for it.

Persisted queries not compatible with Relay

Screenshot

grafik

Configuration:

function subscribeFn(
  operation: RequestParameters,
  variables: Variables
): Observable<any> {
  return Observable.create((sink) => {
    console.log(operation);
    return subscriptionsClient.subscribe(
      {
        query: "",
        variables,
        extensions: {
          persistedQuery: operation.id,
        },
      },
      sink
    );
  });
}

Expected Behaviour

Library is compatible with Relay which requires to set an "id" property in the request parameters (and "query" probably to null).

Actual Behaviour

Relay throws an error because the "id" property is missing and no valid query has been specified. When Relay is configured to use persisted queries, the "operation.text" property is automatically set to null and only the "id" is provided. The "RequestParams" interface does not allow an "id" property and also query cannot be set to null.

https://relay.dev/docs/guides/persisted-queries/

Therefore persisted queries is currently not usable with this library. For fetching I am able to get it working by writing my own fetch function:

const response: Response = await fetch(
    `${process.env.REACT_APP_CORE_API_BASEURL}/graphql`,
    {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        id: operation.id,
        variables,
      }),
    }
  );

Is there any way to get it working with Relay? On my GraphQL server I can make a custom middleware to get the ID out of the extensions, but as long as Relay is throwing an error I can't get it to work at all. Thank you!

graphql-sse/node_modules/graphql-sse/lib/client.mjs:67

Getting this error when using express to start server and using example client code.

graphql-sse

When I change the code in graphql-sse/node_modules/graphql-sse/lib/client.mjs from
const fetchFn = (options.fetchFn || fetch); to const fetchFn = (options.fetchFn); as a workaround to I get a new error saying fetchFn is not a function.

Http2 Example req res params not accepted

Screenshot
Visualising is always helpful.
Screenshot 2022-12-06 at 10 20 03 AM

Expected Behaviour
I expected this to work out of the box, it is the code directly from the examples.

Actual Behaviour
But instead i kept getting an error on line 44 return handler(req, res), which is the graphql-sse createHandler
Screenshot 2022-12-06 at 10 23 21 AM
if you go to the node_modules you'll end up here:
Screenshot 2022-12-06 at 10 24 36 AM

Debug Information
Help us debug the bug?
I am using typescript, strict mode true

Further Information
Anything else you might find helpful.
I was able to bypass not being able to start the server by adding Http2ServerRequest and Http2ServerResponse to the function createHandle, handler and the interface HadlerOptions. But the problem is out of my scrope and my be cascading to other functions im not aware of.

Screenshot 2022-12-06 at 10 25 51 AM

Here is what that looks like:
Screenshot 2022-12-06 at 10 38 23 AM

Infinite retries

Expected Behaviour
The client should only retry up to the specified retry number.

Actual Behaviour
The client retries to infinite and beyond with the error Connection closed while having active streams.

Debug Information
I´m only able to reproduce this problem on my application that I can´t share but the issue lays here https://github.com/enisdenjo/graphql-sse/blob/master/src/client.ts#L476

Before getting the results we clear the number of retries but if the client fails to get the results back (which is the problem in my case) it will keep retrying over and over.

I think the number of the retries should only be cleared after successfully getting the results back.

GraphQL SSE Subscriptions in a React-Native App.

I'm using the urql implementation to with graphql-sse to create a graphql client as follows:

import {
  Client,
  cacheExchange,
  fetchExchange,
  subscriptionExchange,
} from "urql";
import { KEYS, serverDomain } from "../constants";
import { del, retrieve } from "../utils";
import { createClient as createSSEClient } from "graphql-sse";
import { authExchange } from "@urql/exchange-auth";
import { getToken, setToken } from "../state/token";

const sseClient = createSSEClient({
  url: `http://${serverDomain}/graphql`,
});
export const client = new Client({
  url: `http://${serverDomain}/graphql`,
  requestPolicy: "network-only",
  exchanges: [
    cacheExchange,
    fetchExchange,
    authExchange(async (utils) => {
      const jwt = await retrieve(KEYS.TOKEN_KEY);
      setToken(jwt);
      return {
        addAuthToOperation(operation) {
          if (jwt) {
            return utils.appendHeaders(operation, {
              Authorization: `Bearer ${jwt}`,
            });
          }
          return operation;
        },
        willAuthError(_operation) {
          return !jwt;
        },
        didAuthError(error, _operation) {
          return error.graphQLErrors.some(
            (e) => e.extensions?.code === "FORBIDDEN"
          );
        },
        async refreshAuth() {
          setToken(null);
          await del(KEYS.TOKEN_KEY);
        },
      };
    }),
    subscriptionExchange({
      forwardSubscription(operation) {
        return {
          subscribe: (sink) => {
            const dispose = sseClient.subscribe(operation as any, sink);
            return {
              unsubscribe: dispose,
            };
          },
        };
      },
    }),
  ],
  fetchOptions: () => {
    const token = getToken();
    return {
      headers: { authorization: `Bearer ${token || ""}` },
    };
  },
});

Then when try to use the useSubscription hook as follows to listen to new incoming subscriptions in my component as follows:

import { COLORS, FONTS } from "../../constants";
import { AppParamList } from "../../params";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { HomeStack } from "./home";
import { FriendsStack } from "./friends";
import { NotificationsStack } from "./notifications";
import { SettingsStack } from "./settings";
import TabIcon from "../../components/TabIcon/TabIcon";
import {
  MaterialCommunityIcons,
  MaterialIcons,
  Ionicons,
} from "@expo/vector-icons";
import { useMeStore } from "../../store";
import { useSubscription } from "urql";
const Tab = createBottomTabNavigator<AppParamList>();

const Doc = `
  subscription OnNewFriendRequest($input: OnNewFriendRequestInputType!) {
    onNewFriendRequest(input: $input) {
      message
      friend {
        id
        nickname
      }
    }
  }
`;
export const AppTabs = () => {
  const { me } = useMeStore();
  const [{ data, fetching, error }] = useSubscription({
    query: Doc,
    variables: {
      input: { id: "ef7c5256-80f0-4de0-9893-51f3e3e9926e" },
    },
  });

  console.log(JSON.stringify({ data, fetching, error }, null, 2));
  return (
    <Tab.Navigator
      initialRouteName="Home"
      screenOptions={{
        headerShown: false,
        tabBarHideOnKeyboard: true,
        tabBarStyle: {
          elevation: 0,
          shadowOpacity: 0,
          borderTopWidth: 0,
          borderColor: "transparent",
          backgroundColor: COLORS.primary,
          paddingVertical: 10,
          height: 80,
          width: "auto",
        },
        tabBarShowLabel: false,
        tabBarBadgeStyle: {
          backgroundColor: "cornflowerblue",
          color: "white",
          fontSize: 10,
          maxHeight: 20,
          maxWidth: 20,
          marginLeft: 3,
        },
        tabBarVisibilityAnimationConfig: {
          hide: {
            animation: "timing",
          },
          show: {
            animation: "spring",
          },
        },
        tabBarItemStyle: {
          width: "auto",
        },
      }}
    >
      <Tab.Screen
        options={{
          tabBarIcon: (props) => (
            <TabIcon
              {...props}
              title="home"
              Icon={{
                name: "home-account",
                IconComponent: MaterialCommunityIcons,
              }}
            />
          ),
        }}
        name="Home"
        component={HomeStack}
      />
      <Tab.Screen
        options={{
          tabBarIcon: (props) => (
            <TabIcon
              {...props}
              title="friends"
              Icon={{
                name: "person-search",
                IconComponent: MaterialIcons,
              }}
            />
          ),
        }}
        name="Friends"
        component={FriendsStack}
      />
      <Tab.Screen
        options={{
          tabBarIcon: (props) => (
            <TabIcon
              {...props}
              title="notifications"
              Icon={{
                name: "notifications",
                IconComponent: Ionicons,
              }}
            />
          ),
        }}
        name="Notifications"
        component={NotificationsStack}
      />
      <Tab.Screen
        options={{
          tabBarIcon: (props) => (
            <TabIcon
              {...props}
              title="settings"
              Icon={{
                name: "settings",
                IconComponent: Ionicons,
              }}
            />
          ),
        }}
        name="Settings"
        component={SettingsStack}
      />
    </Tab.Navigator>
  );
};

In my logs in a react-native application i'm only getting the loading state to true as follows:

{
  "fetching": true
}

But in my React web app when a new subscription is fired i'm getting the expected results as follows:

{
  "data": {
    "onNewFriendRequest": null
  },
  "fetching": false
}

Why am i getting this different behaviour in react-native?.

[HELP] integrating SSELink and httpLink

I'm trying to integrate SSE with my normal apollo connection to be able to use it with query and subscription.
I've setup the links as follows:

class SSELink extends ApolloLink {
  private client: Client;

  constructor(options: ClientOptions) {
    super();
    this.client = createClient(options);
  }

  public request(operation: Operation): Observable<FetchResult> {
    return new Observable((sink) =>
      this.client.subscribe<FetchResult>(
        { ...operation, query: print(operation.query) },
        {
          next: sink.next.bind(sink),
          complete: sink.complete.bind(sink),
          error: sink.error.bind(sink),
        }
      )
    );
  }
}

const httpLink = new HttpLink({
  uri: "/query",
  credentials: "include",
});
const sselink = new SSELink({
  url: "/query",
  credentials: "include",
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    );
  },
  sselink,
  httpLink
);

const createApolloClient = () =>
  new ApolloClient({
    ssrMode: typeof window === "undefined",
    cache: new InMemoryCache(),
    link: splitLink,
    credentials: "include",
  });

Then I use this client with ApolloProvider and pass this client on.
For the httpLink all is working fine.
For the sseLink it is setting up the call to the backend,
but I'm getting an error in my browser after some time: NS_ERROR_NET_PARTIAL_TRANSFER

Couldn't find anything useful on this error.
Question I have: Can we use mix the links the way I've set it up?

Handler does not accept AST as a query

Hello!

I use graphql-codegen to parse and pre-compile queries into AST (DocumentNode). When I send the query to the server as JSON, I get Invalid query error due to this check.

It looks like both parseReq and prepare are designed12 to handle this properly. So I think this is a bug.

Expected behavior: no error; handler uses provided AST
Current behavior: {"errors":[{"message":"Invalid query"}]}

Example cURL:

curl -v -X POST 'http://0.0.0.0:8000/graphql/kiosk/stream' -H 'accept: text/event-stream' -H 'Accept-Encoding: gzip' -H 'authorization: xxx' -H 'Connection: Keep-Alive' -H 'Content-Length: 505' -H 'Content-Type: application/json; charset=utf-8' -H 'Host: 0.0.0.0:8000' -H 'User-Agent: okhttp/5.0.0-alpha.11' -H 'x-snackpass-client: @snackpass/kiosk' -H 'x-snackpass-client-version: 32.5.0' -d '{"query":{"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"SubscribeToActiveOrders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activePurchases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}}]}}]}}'

Footnotes

  1. https://github.com/enisdenjo/graphql-sse/blob/master/src/handler.ts#L1038

  2. https://github.com/enisdenjo/graphql-sse/blob/master/src/handler.ts#L610

Steps to debug the event flows

Discussed in #19

Originally posted by TestCK March 18, 2022
Is there any way to debug the events coming from server to client.?

I tried looking at the network tab of the browser(chrome), don't seem to get any logs there.

It will be helpful if there is a way to see all the events from server for debugging

Example issues

Screenshot
Visualising is always helpful.
Screenshot 2022-12-07 at 2 33 58 PM
[Codegen image]
This is a codegen file which is connecting to the endpoint where the server with the sse handler is at on endpoint /graphql/stream.

  • An issue that im seeing here is that the client isnt able to connect for that route.
    And there might not be a schema available!
    Even routing to this page through the browser its telling me it's not what i think to expect here, I know it works because other routes operate fine:
    Screenshot 2022-12-07 at 2 36 06 PM
    [Chrome /graphql/stream image]

Here is the server running, it is connecting to "/" where i just test to see the h2 in my Network inspector. According to the documentation.
Screenshot 2022-12-07 at 2 40 40 PM

Im also unsure about the situation on the client side. I've copied the code exactly and removed the headers part for my own purposes!
Screenshot 2022-12-07 at 2 44 08 PM
[Client Side SSELink]

Screenshot 2022-12-07 at 2 44 49 PM
[Final Combination for Apollo provider]

https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition
[Link to Apollo Documentation on linking]

From reading the Apollo link documentation, A directional integration is appropriate,
Screenshot 2022-12-07 at 3 06 47 PM
[SSE integration using a Direction Link] (concept not working)

Expected Behaviour
I expected it to do this -

Actual Behaviour
Im unsure what the server is suppose to return from the handler, but what i expect is probably a schema but currently just unable to connect from the client

Debug Information
Help us debug the bug?

Further Information
This is a React Native project, i know mobile accepts the h2 unsure if its relevant to the sse package!
In the example with the Apollo Client I think it maybe incorrect for h2 because the link isnt using http2!
I understand im not asking a specific question but my real issue it i can't seem to make it work for me!
If we can make it work i would be happy to provide that update.

Doesn't appear to be working properly with Bun

Expected Behaviour
I expected data to be made available as it was being streamed.

Actual Behaviour
but instead data doesn't appear until the stream is completed (all the onnext events fire at once)

Debug Information
using Bun 0.5.8

Further Information
Example code:

import { createHandler } from 'graphql-sse/lib/use/fetch';
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      hello: {
        type: GraphQLString,
        resolve: () => 'world',
      },
    },
  }),
  subscription: new GraphQLObjectType({
    name: 'Subscription',
    fields: {
      greetings: {
        type: GraphQLString,
        subscribe: async function* () {
          for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
            await new Promise((resolve) => setTimeout(resolve, 1000));
            yield { greetings: hi };
          }
        },
      },
    },
  }),
});

const handler = createHandler({ schema });

Bun.serve({
  development: true,
  port: 4000,
  async fetch(req) {
    const [path, _search] = req.url.split('?');

    if (path.endsWith('/graphql/stream')) {
      return await handler(req);
    }
    const response = new Response(null, { status: 404 });
    return response;
  },
});

Example query:

subscription { greetings }

Subscription not getting canceled

I'm using this library with Apollo and after I unsubscribe from the subscription the connection isn't closed. The server doesn't receive any message at all when that happens.

It works fine via Postman or Altair. Is there anything else that needs to be done to cancel/complete the subscription?

missing headers (using Fastify)

We are using @fastify/ cors and cookies and the headers are missing when using this library.

If I change this line (

reply.raw.writeHead(init.status, init.statusText, init.headers);
) like this (

    reply.raw.writeHead(init.status, init.statusText, {...reply.getHeaders(), ...init.headers});

) then the cors headers are not missing, but the cookie header is still missing.

Graphql-helix v1.11 incompatibility

Description

In their last version, graphql-helix decided to change how they handle subscriptions over SSE.

Now, they don't support SSE over POST anymore, and it breaks the way graphql-sse client works with their recommended subscription over SSE
Do you see any alternative to make it work with this library, and more simply I would like to have your POV on that.

Documentation and sample code

I have interest in implementing a kotlin client (for android, possibly multiplatform) version of graphql subscriptions over SSE, and this project seems to be aligned with what I need.

For context pls see apollographql/apollo-kotlin#3756

However I'm not a javascript developer, and the several snippets of code present in the README.md are not really useful for me, since I'd need to pick one, learn the related framework, THEN start my own piece of work. So I'm looking for any standalone sample projects that implement either client or server, ready to be cloned and run from command line without any coding. Are you aware of something like this? I mean, I'd prefer a project + set of instructions that someone that a less than an enthusiastic jr javascript could handle and still be jr at the end of putting it to run. It doesn't help that I'm not fond of js.

On the other hand, are you aware of any client implementations of this protocol not directly related to this github repository?

Steps to debug the event flows

Is there any way to debug the events coming from server to client.?

I tried looking at the network tab of the browser(chrome), don't seem to get any logs there.

It will be helpful if there is a way to see all the events from server for debugging

Client doesn't report the server abruptly going away in the middle of event emission (only in NodeJS)

Kudos, and big thanks, to @brentmjohnson for discovering this bug over at #21!

As per #22 (comment), this issue seems to be exclusive to NodeJS environments.

Expected Behaviour

If the server goes away abruptly during event emission, the client reports the error to the sink.

Actual Behaviour

If the server goes away abruptly during event emission, the client gets stuck and the sink is never finalised.

Headers already sent when errors are thrown from AsyncIterator

I've setup a basic test subscription that throws inside the async iterator:

  type Subscription {
    test: Date!
  }

The subscribe function:

import timers from 'node:timers/promises';
function resolverTest() {
  return (async function* () {
    const interval = timers.setInterval(1000);
    for await (const _ of interval) {
      yield {test: new Date()};
      throw new Error('test');
    }
  })();
}

Server setup:
I've copied it from the fastify TS file. The recipe from the website doc is not in sync and produce the same error but it was easier to debug with this one.

import Fastify from 'fastify';
import { createHandler } from 'graphql-sse/lib/use/fastify';

const handler = createHandler({ schema });
const fastify = Fastify();
fastify.all('/graphql/stream', async (req, reply) => {
  try {
    await handler(req, reply);
  } catch (err) {
    console.error(err);
    reply.code(500).send();
  }
});

fastify.listen({ port: 4000 });
console.log('Listening to port 4000');

When I subscribe to test the server crashes with the following error:
Error [ERR_HTTP_HEADERS_SENT]: Cannot write headers after they are sent to the client

It comes from the reply.code(500).send(). If I remove this line the client hangs indefinitely.

Would it make sense to forward the error into the classic errors property of the response before closing the stream?
Or is there another way to signal the error now that the stream is already open?
Or I am missing something?

Timeout parameter ?

Hi thanks for this implementation of SSE,
i cant find any way to close the connection after some timeout delay is it even possible ?

Empty string in message.event field causes a runtime error.

From RFC8895

The protocol defines three field names: event, id, and data. If a message has more than one "data" line, the value of the data field is the concatenation of the values on those lines. There can be only one "event" and "id" line per message. The "data" field is required; the others are optional.

The check below however fails when message.event === ""

if (!message.event) throw new Error('Missing message event');

Broken persisted queries support

Hi @enisdenjo, first of all, thank you for maintaining such an awesome project!

I would like to report an issue related to persisted queries support.

As described in the recipes:

// 🛸 server
 
import { parse, ExecutionArgs } from 'graphql';
import { createHandler } from 'graphql-sse';
import { schema } from './my-graphql';
 
// a unique GraphQL execution ID used for representing
// a query in the persisted queries store. when subscribing
// you should use the `SubscriptionPayload.query` to transmit the id
type QueryID = string;
 
const queriesStore: Record<QueryID, ExecutionArgs> = {
  iWantTheGreetings: {
    schema, // you may even provide different schemas in the queries store
    document: parse('subscription Greetings { greetings }'),
  },
};
 
export const handler = createHandler({
  onSubscribe: (_req, params) => {
    const persistedQuery =
      queriesStore[String(params.extensions?.persistedQuery)];
    if (persistedQuery) {
      return {
        ...persistedQuery,
        variableValues: params.variables, // use the variables from the client
        contextValue: undefined,
      };
    }
 
    // for extra security only allow the queries from the store
    return [null, { status: 404, statusText: 'Not Found' }];
  },
});

onSubscribe would return a ExecutionArgs result, leading graphql to execute the expected resolver.

However in current implementation, the returned result from above example is treated as ExecutionResult, hence the resolver is not triggered.

graphql-sse/src/handler.ts

Lines 587 to 600 in d4890f1

else if (
isExecutionResult(onSubscribeResult) ||
isAsyncIterable(onSubscribeResult)
)
return {
// even if the result is already available, use
// context because onNext and onComplete needs it
ctx: (typeof context === 'function'
? await context(req, params)
: context) as Context,
perform() {
return onSubscribeResult;
},
};

graphql-sse/src/handler.ts

Lines 1107 to 1110 in d4890f1

function isExecutionResult(val: unknown): val is ExecutionResult {
// TODO: comprehensive check
return isObject(val);
}

Please let me know if I missed anything, and if more details are needed, thank you!

Question related to Inactive Users

Hi there,

Taking a look at this library as a replacement of websocket to use with urql, looks really cool.
Just wondering, if there a way (in graphql-sse or urql) to close SSE connections automatically after a certain time when users are inactive.

Run into the issue in the past with lot of open ws connection to the server, mostly caused by forgotten tabs. Often having to rely on timeout at the load balancer level and limited retry.

Any idea or recommendation to solve this issue with this library ? Any equivalent to keepAlive in graphql-ws ?

my guess is that a frontend solution is probably more accurate at detecting inactivity, can pause the subscriptions, and let the lazy option automatically close the connection.
Where a network solution would be easier to implement but based on arbitrary timeout numbers and can cause some side issues, with active user who will never receive their events because they already reach the timeout.

Cannot call write after a stream was destroyed

Screenshot
image

Expected Behaviour
Do not write to the stream after it is closed / destroyed

Actual Behaviour
Writes to stream after it was destroyed which results in an error.

Debug Information
This bug resulted when using the use/express handler created with a createHandler call. According to the code there is no check in place wether the stream was closed in the meantime.

Further Information
Maybe introduce a variable let cancelled = false; and set it to true once the stream was closed. Check for cancelled before trying to do a .write().

Subscription errors get silently discarded

Hi @enisdenjo, as always, appreciate your efforts in this project.

This time I would like to raise a concern on subscription error handling.

Minimum setup:

schema.ts ( adapted from get-started )

import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';

/**
 * Construct a GraphQL schema and define the necessary resolvers.
 *
 * type Query {
 *   hello: String
 * }
 * type Subscription {
 *   greetings: String
 * }
 */
export const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      hello: {
        type: GraphQLString,
        resolve: () => 'world',
      },
    },
  }),
  subscription: new GraphQLObjectType({
    name: 'Subscription',
    fields: {
      greetings: {
        type: GraphQLString,
        subscribe: async function () {
          // instead of returning the `AsyncIterator`, an error was thrown
          throw new Error('Unexpected stuffs happened');
        },
      },
    },
  }),
});

server.ts ( adapted from this example )

import http from 'http';
import { createHandler } from 'graphql-sse/lib/use/http';
import { schema } from './schema';

// Create the GraphQL over SSE handler
const handler = createHandler({ schema });

// Create an HTTP server using the handler on `/graphql/stream`
const server = http.createServer(async (req, res) => {
  if (req.url.startsWith('/graphql/stream')) {
    try {
      await handler(req, res);
    } catch (error) {
      console.error(error);
      res.writeHead(500).end();

      return;
    }
    res.writeHead(404).end();
  }
});

server.listen(4000);
console.log('Listening to port 4000');

I was expecting the error to be captured and the response to be 500, however what I observed was that the error got silently discarded, and the response was 202.

After a bit digging, I realized the cause is:

graphql-sse/src/handler.ts

Lines 717 to 729 in 3fd31db

async perform() {
const result = await (operation === 'subscription'
? subscribe(args)
: execute(args));
const maybeResult = await onOperation?.(
args.contextValue,
req,
args,
result,
);
if (maybeResult) return maybeResult;
return result;
},

graphql-sse/src/handler.ts

Lines 985 to 1006 in 3fd31db

const result = await prepared.perform();
// operation might have completed before performed
if (!(opId in stream.ops)) {
if (isAsyncGenerator(result)) result.return(undefined);
if (!(opId in stream.ops)) {
return [
null,
{
status: 204,
statusText: 'No Content',
},
];
}
}
if (isAsyncIterable(result)) stream.ops[opId] = result;
// streaming to an empty reservation is ok (will be flushed on connect)
stream.from(prepared.ctx, req, result, opId);
return [null, { status: 202, statusText: 'Accepted' }];

Because an error is thrown, the prepared result here becomes something like { errors: [xxx] } ( from the subscription call ) and finally the 202 response gets returned.

I wonder if this behavior is intended ? if so how we're suppose to capture the errors from subscription ?

Thanks ahead for your kind answers!

Response in ended before the catch in `RequestListener`

In the following example the server is not returning 500 Internal Server Error. Instead the server throws an error (Error [ERR_HTTP_HEADERS_SENT]: Cannot render headers after they are sent to the client) and the process is killed.

import http from 'http';
import { createHandler } from 'graphql-sse';

const handler = createHandler({
  ...
  onNext: () => {
    throw new Error('Not implemented');
  },
});

http.createServer(async (req, res) => {
  try {
    await handler(req, res);
  } catch (err) {
    res.writeHead(500, 'Internal Server Error').end();
  }
});

Special characters in payload are not parsed properly

I'm reporting the issue in this repo because after checking our subscriptions implementation across the whole stack here's where the culprit seems to be.

Screenshot
I had to change the actual data structure for privacy reasons, but basically this is how it looks when logging an event through onMessage. The characters displayed as � are characters with diacriticals such as á, é, í, ó, ú, ñ etc.

{
    "event": "next",
    "data": {
        "data": {
            "update": {
                "someField": {
                    "id": "1234",
                    "user": {
                        "id": "5678",
                        "fullName": "Abcdef � ��",
                        "__typename": "User"
                    },
                    "__typename": "SomeField"
                },
                "__typename": "Update"
            }
        },
        "errors": [],
        "subId": "08738bd4-57e2-4316-8d99-98ca3975100c",
        "type": "SUBSCRIPTION_DATA"
    }
}

Expected Behaviour
Special characters are encoded properly.

Actual Behaviour
They are not.

Debug Information
If you tell me how, happy to add more info.

Further Information
We're using the implementation with Apollo Client, as explained in the README.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.