Skip to content

Commit

Permalink
fix: multipart response specification compatibility (#3400)
Browse files Browse the repository at this point in the history
* chore: add integration test for fetch-multipart-graphql

* fix

* add changeset

* no snap

* typo
  • Loading branch information
n1ru4l authored Aug 3, 2024
1 parent c07e1ca commit 0866c1b
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 22 deletions.
7 changes: 7 additions & 0 deletions .changeset/light-pants-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'graphql-yoga': patch
---

Restores compatibility with [RFC1341: The Multipart Content-Type](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) by including preceding `\r\n` for initial boundary delimiter when using the multipart response protocol.

This makes Yoga compatible with libraries that strictly follow the response protocol, such as [fetch-multipart-graphql](https://github.com/relay-tools/fetch-multipart-graphql).
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Defer / Stream defer: defer 1`] = `
"---
"
---
Content-Type: application/json; charset=utf-8
Content-Length: 50

Expand All @@ -16,7 +17,8 @@ Content-Length: 78
`;

exports[`Defer / Stream stream: stream 1`] = `
"---
"
---
Content-Type: application/json; charset=utf-8
Content-Length: 39

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function processMultipartResult(result: ResultProcessorInput, fetchAPI: F
},
};
}
controller.enqueue(textEncoder.encode('\r\n'));
controller.enqueue(textEncoder.encode(`---`));
},
async pull(controller) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createServer, get, IncomingMessage } from 'node:http';
import { AddressInfo } from 'node:net';
import { setTimeout as setTimeout$ } from 'node:timers/promises';
import fetchMultipart from 'fetch-multipart-graphql';
import { createLogger, createSchema, createYoga, useExecutionCancellation } from 'graphql-yoga';
import { useDeferStream } from '@graphql-yoga/plugin-defer-stream';
import { createPushPullAsyncIterable } from '../__tests__/push-pull-async-iterable.js';
Expand Down Expand Up @@ -165,13 +166,14 @@ it('memory/cleanup leak by source that never publishes a value', async () => {

const chunkStr = Buffer.from(next.value).toString('utf-8');
expect(chunkStr).toMatchInlineSnapshot(`
"---
Content-Type: application/json; charset=utf-8
Content-Length: 33
"
---
Content-Type: application/json; charset=utf-8
Content-Length: 33

{"data":{"hi":[]},"hasNext":true}
---"
`);
{"data":{"hi":[]},"hasNext":true}
---"
`);

await expect(iterator.next()).rejects.toMatchInlineSnapshot(`[Error: aborted]`);

Expand All @@ -194,3 +196,94 @@ it('memory/cleanup leak by source that never publishes a value', async () => {
});
}
});

describe('fetch-multipart-graphql', () => {
it('execute defer operation', async () => {
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
a: String
b: String
}
`,
resolvers: {
Query: {
a: async () => {
return 'a';
},
b: async () => {
return 'b';
},
},
},
}),
plugins: [useDeferStream()],
});

const server = createServer(yoga);

try {
await new Promise<void>(resolve => {
server.listen(() => {
resolve();
});
});

const port = (server.address() as AddressInfo)?.port ?? null;
if (port === null) {
throw new Error('Missing port...');
}

await new Promise<void>((resolve, reject) => {
fetchMultipart(`http://localhost:${port}/graphql`, {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'multipart/mixed',
},
body: JSON.stringify({
query: /* GraphQL */ `
query {
... on Query @defer {
a
}
}
`,
}),
onNext(next) {
expect(next).toEqual([
{
data: {},
hasNext: true,
},
{
hasNext: false,
incremental: [
{
data: {
a: 'a',
},
path: [],
},
],
},
]);
},
onError(err) {
reject(err);
},
onComplete() {
resolve();
},
});
});
} finally {
await new Promise<void>(res => {
server.close(() => {
res();
});
});
}
});
});
30 changes: 16 additions & 14 deletions packages/plugins/defer-stream/__tests__/defer-stream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,20 @@ describe('Defer/Stream', () => {
const finalText = await response.text();

expect(finalText).toMatchInlineSnapshot(`
"---
Content-Type: application/json; charset=utf-8
Content-Length: 26

{"data":{},"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 74

{"incremental":[{"data":{"goodbye":"goodbye"},"path":[]}],"hasNext":false}
-----
"
`);
"
---
Content-Type: application/json; charset=utf-8
Content-Length: 26

{"data":{},"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 74

{"incremental":[{"data":{"goodbye":"goodbye"},"path":[]}],"hasNext":false}
-----
"
`);
});

it('should execute on stream directive', async () => {
Expand All @@ -140,7 +141,8 @@ describe('Defer/Stream', () => {
const finalText = await response.text();

expect(finalText).toMatchInlineSnapshot(`
"---
"
---
Content-Type: application/json; charset=utf-8
Content-Length: 44

Expand Down
1 change: 1 addition & 0 deletions packages/plugins/defer-stream/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"devDependencies": {
"@graphql-tools/executor-http": "^1.0.4",
"@whatwg-node/fetch": "^0.9.17",
"fetch-multipart-graphql": "3.2.1",
"graphql": "^16.6.0",
"graphql-yoga": "workspace:*",
"tslib": "^2.5.2"
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0866c1b

Please sign in to comment.