enisdenjo / graphql-sse Goto Github PK
View Code? Open in Web Editor NEWZero-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
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
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 }
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.
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:
And more.
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.
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
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.
Lines 587 to 600 in d4890f1
Lines 1107 to 1110 in d4890f1
Please let me know if I missed anything, and if more details are needed, thank you!
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}));
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?
Screenshot
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!
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.
Make sure all supported options are passed to the fetch function.
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.
Hi thanks for this implementation of SSE,
i cant find any way to close the connection after some timeout delay is it even possible ?
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()
.
Screenshot
Visualising is always helpful.
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
if you go to the node_modules you'll end up here:
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.
We are using @fastify/
cors
and cookies
and the headers are missing when using this library.
If I change this line (
graphql-sse/src/use/fastify.ts
Line 90 in 061b0fc
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.
Follow-up issue of #27.
Expected Behaviour
Network errors during event emission are retried in Safari.
Actual Behaviour
Network errors during event emission are not retried in Safari.
Debug Information
If a network error occurs during event emission in Safari, the connection is completed (instead of errored out).
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
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.
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?.
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
ERR_NETWORK_CHANGED
Pleas let me know if you aren't able to reproduce and whether I can help with anything.
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.
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?
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?
Screenshot
Visualising is always helpful.
[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.
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.
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!
[Client Side SSELink]
[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,
[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.
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 === ""
Line 88 in b4019e4
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:
Lines 717 to 729 in 3fd31db
Lines 985 to 1006 in 3fd31db
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!
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();
}
});
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.
Getting this error when using express
to start server and using example client code.
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.
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"}}]}}]}}]}}'
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?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.