Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit b060480

Browse files
committed
call return on underlying async iterator when connection closes
1 parent aa62e24 commit b060480

File tree

2 files changed

+169
-14
lines changed

2 files changed

+169
-14
lines changed

src/__tests__/http-test.ts

+133-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import zlib from 'zlib';
2+
import type http from 'http';
23

34
import type { Server as Restify } from 'restify';
45
import connect from 'connect';
@@ -81,6 +82,12 @@ function urlString(urlParams?: { [param: string]: string }): string {
8182
return string;
8283
}
8384

85+
function sleep() {
86+
return new Promise((r) => {
87+
setTimeout(r, 1);
88+
});
89+
}
90+
8491
describe('GraphQL-HTTP tests for connect', () => {
8592
runTests(() => {
8693
const app = connect();
@@ -2389,9 +2396,7 @@ function runTests(server: Server) {
23892396
graphqlHTTP(() => ({
23902397
schema: TestSchema,
23912398
async *customExecuteFn() {
2392-
await new Promise((r) => {
2393-
setTimeout(r, 1);
2394-
});
2399+
await sleep();
23952400
yield {
23962401
data: {
23972402
test2: 'Modification',
@@ -2436,6 +2441,131 @@ function runTests(server: Server) {
24362441
].join('\r\n'),
24372442
);
24382443
});
2444+
2445+
it('calls return on underlying async iterable when connection is closed', async () => {
2446+
const app = server();
2447+
const fakeReturn = sinon.fake();
2448+
2449+
app.get(
2450+
urlString(),
2451+
graphqlHTTP(() => ({
2452+
schema: TestSchema,
2453+
// custom iterable keeps yielding until return is called
2454+
customExecuteFn() {
2455+
let returned = false;
2456+
return {
2457+
[Symbol.asyncIterator]: () => ({
2458+
next: async () => {
2459+
await sleep();
2460+
if (returned) {
2461+
return { value: undefined, done: true };
2462+
}
2463+
return {
2464+
value: { data: { test: 'Hello, World' }, hasNext: true },
2465+
done: false,
2466+
};
2467+
},
2468+
return: () => {
2469+
returned = true;
2470+
fakeReturn();
2471+
return Promise.resolve({ value: undefined, done: true });
2472+
},
2473+
}),
2474+
};
2475+
},
2476+
})),
2477+
);
2478+
2479+
const request = app
2480+
.request()
2481+
.get(urlString({ query: '{test}' }))
2482+
.parse((res, cb) => {
2483+
res.on('data', (data) => {
2484+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
2485+
((res as unknown) as http.IncomingMessage).destroy();
2486+
});
2487+
res.on('end', (err) => {
2488+
cb(err, null);
2489+
});
2490+
});
2491+
2492+
const response = await request;
2493+
await sleep();
2494+
expect(response.status).to.equal(200);
2495+
expect(response.text).to.equal(
2496+
[
2497+
'',
2498+
'---',
2499+
'Content-Type: application/json; charset=utf-8',
2500+
'Content-Length: 47',
2501+
'',
2502+
'{"data":{"test":"Hello, World"},"hasNext":true}',
2503+
'',
2504+
].join('\r\n'),
2505+
);
2506+
expect(fakeReturn.callCount).to.equal(1);
2507+
});
2508+
2509+
it('handles return function on async iterable that throws', async () => {
2510+
const app = server();
2511+
2512+
app.get(
2513+
urlString(),
2514+
graphqlHTTP(() => ({
2515+
schema: TestSchema,
2516+
// custom iterable keeps yielding until return is called
2517+
customExecuteFn() {
2518+
let returned = false;
2519+
return {
2520+
[Symbol.asyncIterator]: () => ({
2521+
next: async () => {
2522+
await sleep();
2523+
if (returned) {
2524+
return { value: undefined, done: true };
2525+
}
2526+
return {
2527+
value: { data: { test: 'Hello, World' }, hasNext: true },
2528+
done: false,
2529+
};
2530+
},
2531+
return: () => {
2532+
returned = true;
2533+
return Promise.reject(new Error('Throws!'));
2534+
},
2535+
}),
2536+
};
2537+
},
2538+
})),
2539+
);
2540+
2541+
const request = app
2542+
.request()
2543+
.get(urlString({ query: '{test}' }))
2544+
.parse((res, cb) => {
2545+
res.on('data', (data) => {
2546+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
2547+
((res as unknown) as http.IncomingMessage).destroy();
2548+
});
2549+
res.on('end', (err) => {
2550+
cb(err, null);
2551+
});
2552+
});
2553+
2554+
const response = await request;
2555+
await sleep();
2556+
expect(response.status).to.equal(200);
2557+
expect(response.text).to.equal(
2558+
[
2559+
'',
2560+
'---',
2561+
'Content-Type: application/json; charset=utf-8',
2562+
'Content-Length: 47',
2563+
'',
2564+
'{"data":{"test":"Hello, World"},"hasNext":true}',
2565+
'',
2566+
].join('\r\n'),
2567+
);
2568+
});
24392569
});
24402570

24412571
describe('Custom parse function', () => {

src/index.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ export function graphqlHTTP(options: Options): Middleware {
213213
let documentAST: DocumentNode;
214214
let executeResult;
215215
let result: ExecutionResult;
216+
let finishedIterable = false;
216217

217218
try {
218219
// Parse the Request to get GraphQL request parameters.
@@ -371,6 +372,23 @@ export function graphqlHTTP(options: Options): Middleware {
371372
const asyncIterator = getAsyncIterator<ExecutionResult>(
372373
executeResult,
373374
);
375+
376+
response.on('close', () => {
377+
if (
378+
!finishedIterable &&
379+
typeof asyncIterator.return === 'function'
380+
) {
381+
asyncIterator.return().then(null, (rawError: unknown) => {
382+
const graphqlError = getGraphQlError(rawError);
383+
sendPartialResponse(pretty, response, {
384+
data: undefined,
385+
errors: [formatErrorFn(graphqlError)],
386+
hasNext: false,
387+
});
388+
});
389+
}
390+
});
391+
374392
const { value } = await asyncIterator.next();
375393
result = value;
376394
} else {
@@ -398,6 +416,7 @@ export function graphqlHTTP(options: Options): Middleware {
398416
rawError instanceof Error ? rawError : String(rawError),
399417
);
400418

419+
// eslint-disable-next-line require-atomic-updates
401420
response.statusCode = error.status;
402421

403422
const { headers } = error;
@@ -431,6 +450,7 @@ export function graphqlHTTP(options: Options): Middleware {
431450
// the resulting JSON payload.
432451
// https://graphql.github.io/graphql-spec/#sec-Data
433452
if (response.statusCode === 200 && result.data == null) {
453+
// eslint-disable-next-line require-atomic-updates
434454
response.statusCode = 500;
435455
}
436456

@@ -462,17 +482,7 @@ export function graphqlHTTP(options: Options): Middleware {
462482
sendPartialResponse(pretty, response, formattedPayload);
463483
}
464484
} catch (rawError: unknown) {
465-
/* istanbul ignore next: Thrown by underlying library. */
466-
const error =
467-
rawError instanceof Error ? rawError : new Error(String(rawError));
468-
const graphqlError = new GraphQLError(
469-
error.message,
470-
undefined,
471-
undefined,
472-
undefined,
473-
undefined,
474-
error,
475-
);
485+
const graphqlError = getGraphQlError(rawError);
476486
sendPartialResponse(pretty, response, {
477487
data: undefined,
478488
errors: [formatErrorFn(graphqlError)],
@@ -481,6 +491,7 @@ export function graphqlHTTP(options: Options): Middleware {
481491
}
482492
response.write('\r\n-----\r\n');
483493
response.end();
494+
finishedIterable = true;
484495
return;
485496
}
486497

@@ -657,3 +668,17 @@ function getAsyncIterator<T>(
657668
const method = asyncIterable[Symbol.asyncIterator];
658669
return method.call(asyncIterable);
659670
}
671+
672+
function getGraphQlError(rawError: unknown) {
673+
/* istanbul ignore next: Thrown by underlying library. */
674+
const error =
675+
rawError instanceof Error ? rawError : new Error(String(rawError));
676+
return new GraphQLError(
677+
error.message,
678+
undefined,
679+
undefined,
680+
undefined,
681+
undefined,
682+
error,
683+
);
684+
}

0 commit comments

Comments
 (0)