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

Commit

Permalink
call return on underlying async iterator when connection closes
Browse files Browse the repository at this point in the history
  • Loading branch information
robrichard committed Nov 17, 2020
1 parent aa62e24 commit d883ae7
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 14 deletions.
146 changes: 143 additions & 3 deletions src/__tests__/http-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import zlib from 'zlib';
import type http from 'http';

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

function sleep() {
return new Promise((r) => {
setTimeout(r, 1);
});
}

describe('GraphQL-HTTP tests for connect', () => {
runTests(() => {
const app = connect();
Expand Down Expand Up @@ -2389,9 +2396,7 @@ function runTests(server: Server) {
graphqlHTTP(() => ({
schema: TestSchema,
async *customExecuteFn() {
await new Promise((r) => {
setTimeout(r, 1);
});
await sleep();
yield {
data: {
test2: 'Modification',
Expand Down Expand Up @@ -2436,6 +2441,141 @@ function runTests(server: Server) {
].join('\r\n'),
);
});

it('calls return on underlying async iterable when connection is closed', async () => {
const app = server();
const fakeReturn = sinon.fake();

app.get(
urlString(),
graphqlHTTP(() => ({
schema: TestSchema,
// custom iterable keeps yielding until return is called
customExecuteFn() {
let returned = false;
return {
[Symbol.asyncIterator]: () => ({
next: async () => {
await sleep();
if (returned) {
return { value: undefined, done: true };
}
return {
value: { data: { test: 'Hello, World' }, hasNext: true },
done: false,
};
},
return: () => {
returned = true;
fakeReturn();
return Promise.resolve({ value: undefined, done: true });
},
}),
};
},
})),
);

let text = '';
const request = app
.request()
.get(urlString({ query: '{test}' }))
.parse((res, cb) => {
res.on('data', (data) => {
text = `${text}${data.toString('utf8') as string}`;
((res as unknown) as http.IncomingMessage).destroy();
});
res.on('end', (err) => {
cb(err, null);
});
});

try {
await request;
} catch (e: unknown) {
// ignore aborted error
}
// sleep to allow return function to be called
await sleep();
expect(text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 47',
'',
'{"data":{"test":"Hello, World"},"hasNext":true}',
'',
].join('\r\n'),
);
expect(fakeReturn.callCount).to.equal(1);
});

it('handles return function on async iterable that throws', async () => {
const app = server();

app.get(
urlString(),
graphqlHTTP(() => ({
schema: TestSchema,
// custom iterable keeps yielding until return is called
customExecuteFn() {
let returned = false;
return {
[Symbol.asyncIterator]: () => ({
next: async () => {
await sleep();
if (returned) {
return { value: undefined, done: true };
}
return {
value: { data: { test: 'Hello, World' }, hasNext: true },
done: false,
};
},
return: () => {
returned = true;
return Promise.reject(new Error('Throws!'));
},
}),
};
},
})),
);

let text = '';
const request = app
.request()
.get(urlString({ query: '{test}' }))
.parse((res, cb) => {
res.on('data', (data) => {
text = `${text}${data.toString('utf8') as string}`;
((res as unknown) as http.IncomingMessage).destroy();
});
res.on('end', (err) => {
cb(err, null);
});
});

try {
await request;
} catch (e: unknown) {
// ignore aborted error
}
// sleep to allow return function to be called
await sleep();
expect(text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 47',
'',
'{"data":{"test":"Hello, World"},"hasNext":true}',
'',
].join('\r\n'),
);
});
});

describe('Custom parse function', () => {
Expand Down
47 changes: 36 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export function graphqlHTTP(options: Options): Middleware {
let documentAST: DocumentNode;
let executeResult;
let result: ExecutionResult;
let finishedIterable = false;

try {
// Parse the Request to get GraphQL request parameters.
Expand Down Expand Up @@ -371,6 +372,23 @@ export function graphqlHTTP(options: Options): Middleware {
const asyncIterator = getAsyncIterator<ExecutionResult>(
executeResult,
);

response.on('close', () => {
if (
!finishedIterable &&
typeof asyncIterator.return === 'function'
) {
asyncIterator.return().then(null, (rawError: unknown) => {
const graphqlError = getGraphQlError(rawError);
sendPartialResponse(pretty, response, {
data: undefined,
errors: [formatErrorFn(graphqlError)],
hasNext: false,
});
});
}
});

const { value } = await asyncIterator.next();
result = value;
} else {
Expand Down Expand Up @@ -398,6 +416,7 @@ export function graphqlHTTP(options: Options): Middleware {
rawError instanceof Error ? rawError : String(rawError),
);

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

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

Expand Down Expand Up @@ -462,17 +482,7 @@ export function graphqlHTTP(options: Options): Middleware {
sendPartialResponse(pretty, response, formattedPayload);
}
} catch (rawError: unknown) {
/* istanbul ignore next: Thrown by underlying library. */
const error =
rawError instanceof Error ? rawError : new Error(String(rawError));
const graphqlError = new GraphQLError(
error.message,
undefined,
undefined,
undefined,
undefined,
error,
);
const graphqlError = getGraphQlError(rawError);
sendPartialResponse(pretty, response, {
data: undefined,
errors: [formatErrorFn(graphqlError)],
Expand All @@ -481,6 +491,7 @@ export function graphqlHTTP(options: Options): Middleware {
}
response.write('\r\n-----\r\n');
response.end();
finishedIterable = true;
return;
}

Expand Down Expand Up @@ -657,3 +668,17 @@ function getAsyncIterator<T>(
const method = asyncIterable[Symbol.asyncIterator];
return method.call(asyncIterable);
}

function getGraphQlError(rawError: unknown) {
/* istanbul ignore next: Thrown by underlying library. */
const error =
rawError instanceof Error ? rawError : new Error(String(rawError));
return new GraphQLError(
error.message,
undefined,
undefined,
undefined,
undefined,
error,
);
}

0 comments on commit d883ae7

Please sign in to comment.