Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/graphql-yoga-3197-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'graphql-yoga': patch
---
dependencies updates:
- Updated dependency [`@graphql-tools/executor@^1.2.1`
↗︎](https://www.npmjs.com/package/@graphql-tools/executor/v/1.2.1) (from `^1.0.0`, in
`dependencies`)
- Updated dependency [`@whatwg-node/server@^0.9.27`
↗︎](https://www.npmjs.com/package/@whatwg-node/server/v/0.9.27) (from `^0.9.1`, in
`dependencies`)
8 changes: 8 additions & 0 deletions .changeset/green-badgers-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'graphql-yoga': minor
---

Abort GraphQL execution when HTTP request is canceled.

The execution of subsequent GraphQL resolvers is now aborted if the incoming HTTP request is canceled from the client side.
This reduces the load of your API in case incoming requests with deep GraphQL operation selection sets are canceled.
2 changes: 1 addition & 1 deletion examples/apollo-federation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
},
"devDependencies": {
"@apollo/gateway": "2.4.7",
"@whatwg-node/fetch": "^0.9.0"
"@whatwg-node/fetch": "^0.9.17"
}
}
2 changes: 1 addition & 1 deletion examples/bun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"graphql-yoga": "5.2.0"
},
"devDependencies": {
"@whatwg-node/fetch": "^0.9.0"
"@whatwg-node/fetch": "^0.9.17"
}
}
2 changes: 1 addition & 1 deletion examples/cloudflare-modules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"devDependencies": {
"@cloudflare/workers-types": "4.20230518.0",
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"typescript": "5.1.6",
"wrangler": "3.1.0"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/error-handling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"start": "ts-node src/index.ts"
},
"dependencies": {
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"graphql": "^16.1.0",
"graphql-yoga": "5.2.0"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/file-upload-nextjs-pothos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"devDependencies": {
"@types/react": "^18.0.17",
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"eslint": "8.42.0",
"eslint-config-next": "13.4.12",
"typescript": "5.1.6"
Expand Down
2 changes: 1 addition & 1 deletion examples/file-upload-nexus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"nexus": "^1.3.0"
},
"devDependencies": {
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"ts-node": "10.9.1",
"typescript": "5.1.6"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/file-upload/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"ts-node": "10.9.1"
},
"devDependencies": {
"@whatwg-node/fetch": "^0.9.0"
"@whatwg-node/fetch": "^0.9.17"
}
}
2 changes: 1 addition & 1 deletion examples/generic-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"devDependencies": {
"@types/node": "18.16.16",
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"cross-env": "7.0.3",
"ts-node": "10.9.1",
"ts-node-dev": "2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/hapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"graphql-yoga": "5.2.0"
},
"devDependencies": {
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"ts-node": "10.9.1",
"typescript": "5.1.6"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,9 @@ describe('NextJS Legacy Pages', () => {
...Object.fromEntries(response.headers.entries()),
date: null,
'keep-alive': null,
}).toMatchInlineSnapshot(`
{
"connection": "close",
"content-length": "79",
"content-type": "application/json; charset=utf-8",
"date": null,
"keep-alive": null,
"vary": "Accept-Encoding",
}
`);
}).toMatchObject({
'content-type': 'application/json; charset=utf-8',
});

const json = await response.json();

Expand Down
2 changes: 1 addition & 1 deletion examples/response-cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"devDependencies": {
"@types/node": "18.16.16",
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"cross-env": "7.0.3",
"ts-node": "10.9.1",
"ts-node-dev": "2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/service-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"graphql-yoga": "5.2.0"
},
"devDependencies": {
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"typescript": "5.1.6",
"wrangler": "3.1.0"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/uwebsockets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"devDependencies": {
"@types/ws": "8.5.4",
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"ws": "8.13.0"
}
}
7 changes: 4 additions & 3 deletions packages/event-target/redis-event-target/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ export function createRedisEventTarget<TEvent extends CustomEvent>(
if (callbacks === undefined) {
callbacks = new Set();
callbacksForTopic.set(topic, callbacks);

subscribeClient.subscribe(topic);
callbacks.add(callback);
return subscribeClient.subscribe(topic).then(() => undefined);
}
callbacks.add(callback);
return;
}

function removeCallback(topic: string, callback: (event: TEvent) => void) {
Expand All @@ -65,7 +66,7 @@ export function createRedisEventTarget<TEvent extends CustomEvent>(
if (callbackOrOptions != null) {
const callback =
'handleEvent' in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions;
addCallback(topic, callback);
return addCallback(topic, callback);
}
},
dispatchEvent(event: TEvent) {
Expand Down
5 changes: 4 additions & 1 deletion packages/event-target/typed-event-target/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ export type TypedEventListenerOrEventListenerObject<TEvent extends CustomEvent>
| TypedEventListenerObject<TEvent>;

export interface TypedEventTarget<TEvent extends CustomEvent> extends EventTarget {
/**
* If the return value is a promise, the promise will resolve once the event listener has been set up.
*/
addEventListener(
type: string,
callback: TypedEventListenerOrEventListenerObject<TEvent> | null,
options?: AddEventListenerOptions | boolean,
): void;
): void | Promise<void>;
dispatchEvent(event: TEvent): boolean;
removeEventListener(
type: string,
Expand Down
65 changes: 64 additions & 1 deletion packages/graphql-yoga/__tests__/error-masking.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inspect } from '@graphql-tools/utils';
import { createGraphQLError, createSchema, createYoga } from '../src/index.js';
import { createGraphQLError, createLogger, createSchema, createYoga } from '../src/index.js';
import { eventStream } from './utilities.js';

describe('error masking', () => {
Expand Down Expand Up @@ -693,4 +693,67 @@ describe('error masking', () => {

expect(counter).toBe(3);
});

it('AbortSignal cancelation within resolver is not treated as a execution request cancelation by the yoga error handler', async () => {
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
root: A!
}
type A {
a: String!
}
`,
resolvers: {
Query: {
async root() {
/** we just gonna throw a DOMException here to see what happens */
const abortController = new AbortController();
abortController.abort();
expect(abortController.signal.reason?.constructor.name).toBe('DOMException');
throw abortController.signal.reason;
},
},
},
});

const logger = createLogger('silent');
const error = jest.fn();
const debug = jest.fn();
logger.debug = debug;
logger.error = error;
const yoga = createYoga({ schema, logging: logger });

const result = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
body: JSON.stringify({ query: '{ root { a } }' }),
headers: {
'Content-Type': 'application/json',
},
});

expect(result.status).toEqual(200);
expect(await result.json()).toEqual({
data: null,
errors: [
{
locations: [
{
column: 3,
line: 1,
},
],
message: 'Unexpected error.',
path: ['root'],
},
],
});
// in the future this might change as we decide to within our graphql-tools/executor error handler treat DOMException similar to a normal Error
expect(error.mock.calls).toMatchObject([[{ message: 'Unexpected error value: {}' }]]);
expect(debug.mock.calls).toEqual([
['Parsing request to extract GraphQL parameters'],
['Processing GraphQL Parameters'],
['Processing GraphQL Parameters done.'],
]);
});
});
70 changes: 70 additions & 0 deletions packages/graphql-yoga/__tests__/request-cancellation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createSchema, createYoga } from '../src/index';

describe('request cancellation', () => {
it('request cancellation stops invocation of subsequent resolvers', async () => {
const rootResolverGotInvokedD = createDeferred();
const requestGotCancelledD = createDeferred();
let aResolverGotInvoked = false;
let rootResolverGotInvoked = false;
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
root: A!
}
type A {
a: String!
}
`,
resolvers: {
Query: {
async root() {
rootResolverGotInvoked = true;
rootResolverGotInvokedD.resolve();
await requestGotCancelledD.promise;
return { a: 'a' };
},
},
A: {
a() {
aResolverGotInvoked = true;
return 'a';
},
},
},
});
const yoga = createYoga({ schema });
const abortController = new AbortController();
const promise = Promise.resolve(
yoga.fetch('http://yoga/graphql', {
method: 'POST',
body: JSON.stringify({ query: '{ root { a } }' }),
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal,
}),
);
await rootResolverGotInvokedD.promise;
abortController.abort();
requestGotCancelledD.resolve();
await expect(promise).rejects.toThrow('This operation was aborted');
expect(rootResolverGotInvoked).toBe(true);
expect(aResolverGotInvoked).toBe(false);
await requestGotCancelledD.promise;
});
});

type Deferred<T = void> = {
resolve: (value: T) => void;
reject: (value: unknown) => void;
promise: Promise<T>;
};

function createDeferred<T = void>(): Deferred<T> {
const d = {} as Deferred<T>;
d.promise = new Promise<T>((resolve, reject) => {
d.resolve = resolve;
d.reject = reject;
});
return d;
}
Loading