Skip to content

Commit 1dc4695

Browse files
committed
Fix async context being lost in websocket connections
subscribe() is called from a socket.onmessage event, which doesn't have the same async context as the original http (upgrade) request. Save and resume the context between these two points. This allows HttpHooks to declare an ALS (like edgedb current user), and then have these scoped inside GQL subscriptions from websockets.
1 parent 9280a6b commit 1dc4695

File tree

1 file changed

+21
-5
lines changed

1 file changed

+21
-5
lines changed

src/core/graphql/driver.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type YogaServerInstance,
1414
type YogaServerOptions,
1515
} from 'graphql-yoga';
16+
import { AsyncResource } from 'node:async_hooks';
1617
import type { WebSocket } from 'ws';
1718
import { type GqlContextType } from '~/common';
1819
import { HttpAdapter, type IRequest } from '../http';
@@ -104,7 +105,9 @@ export class Driver extends AbstractDriver<DriverConfig> {
104105
* So this allows our "yoga" plugins to be executed.
105106
*/
106107
private makeWsHandler(options: DriverConfig) {
108+
const asyncContextBySocket = new WeakMap<WebSocket, AsyncResource>();
107109
interface WsExecutionArgs extends ExecutionArgs {
110+
socket: WebSocket;
108111
envelop: ReturnType<ReturnType<typeof envelop>>;
109112
}
110113

@@ -113,20 +116,27 @@ export class Driver extends AbstractDriver<DriverConfig> {
113116
// This forwards to yoga/envelop.
114117
// This was adapted from yoga's graphql-ws example.
115118
// https://github.com/dotansimha/graphql-yoga/tree/main/examples/graphql-ws
116-
const wsHandler = makeGqlWSHandler<
119+
const fastifyWsHandler = makeGqlWSHandler<
117120
Record<string, unknown>,
118121
{ socket: WebSocket; request: IRequest }
119122
>({
120123
schema: options.schema!,
121124
// Custom execute/subscribe functions that really just defer to a
122125
// unique envelop (yoga) instance per request.
123126
execute: (wsArgs) => {
124-
const { envelop, ...args } = wsArgs as WsExecutionArgs;
125-
return envelop.execute(args);
127+
const { envelop, socket, ...args } = wsArgs as WsExecutionArgs;
128+
return asyncContextBySocket.get(socket)!.runInAsyncScope(() => {
129+
return envelop.execute(args);
130+
});
126131
},
127132
subscribe: (wsArgs) => {
128-
const { envelop, ...args } = wsArgs as WsExecutionArgs;
129-
return envelop.subscribe(args);
133+
const { envelop, socket, ...args } = wsArgs as WsExecutionArgs;
134+
// Because this is called via socket.onmessage, we don't have
135+
// the same async context we started with.
136+
// Grab and resume it.
137+
return asyncContextBySocket.get(socket)!.runInAsyncScope(() => {
138+
return envelop.subscribe(args);
139+
});
130140
},
131141
// Create a unique envelop/yoga instance for each subscription.
132142
// This allows "yoga" plugins that are really just envelop hooks
@@ -151,6 +161,7 @@ export class Driver extends AbstractDriver<DriverConfig> {
151161
// Public examples put these functions in the context, but I don't
152162
// like exposing that implementation detail to the rest of the app.
153163
envelop,
164+
socket,
154165
};
155166

156167
const errors = envelop.validate(args.schema, args.document);
@@ -161,6 +172,11 @@ export class Driver extends AbstractDriver<DriverConfig> {
161172
},
162173
});
163174

175+
const wsHandler: FastifyRoute['wsHandler'] = function (socket, req) {
176+
// Save a reference to the current async context, so we can resume it.
177+
asyncContextBySocket.set(socket, new AsyncResource('graphql-ws'));
178+
return fastifyWsHandler.call(this, socket, req);
179+
};
164180
return wsHandler;
165181
}
166182

0 commit comments

Comments
 (0)