diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index 07f8e7d404e..d1927026576 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -1304,205 +1304,310 @@ describe('HttpLink', () => { globalThis.TextDecoder = originalTextDecoder; }); - const body = [ - '---', - 'Content-Type: application/json; charset=utf-8', - 'Content-Length: 43', - '', - '{"data":{"stub":{"id":"0"}},"hasNext":true}', - '---', - 'Content-Type: application/json; charset=utf-8', - 'Content-Length: 58', - '', - '{"hasNext":false, "incremental": [{"data":{"name":"stubby"},"path":["stub"],"extensions":{"timestamp":1633038919}}]}', - '-----', - ].join("\r\n"); - - it('can handle whatwg stream bodies', (done) => { - const stream = new ReadableStream({ - async start(controller) { - const lines = body.split("\r\n"); - try { - for (const line of lines) { - await new Promise((resolve) => setTimeout(resolve, 10)); - controller.enqueue(line + "\r\n"); + describe('@defer', () => { + const body = [ + '---', + 'Content-Type: application/json; charset=utf-8', + 'Content-Length: 43', + '', + '{"data":{"stub":{"id":"0"}},"hasNext":true}', + '---', + 'Content-Type: application/json; charset=utf-8', + 'Content-Length: 58', + '', + '{"hasNext":false, "incremental": [{"data":{"name":"stubby"},"path":["stub"],"extensions":{"timestamp":1633038919}}]}', + '-----', + ].join("\r\n"); + + it('can handle whatwg stream bodies', (done) => { + const stream = new ReadableStream({ + async start(controller) { + const lines = body.split("\r\n"); + try { + for (const line of lines) { + await new Promise((resolve) => setTimeout(resolve, 10)); + controller.enqueue(line + "\r\n"); + } + } finally { + controller.close(); } - } finally { - controller.close(); - } - }, - }); + }, + }); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ 'content-type': 'multipart/mixed' }), - })); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ 'content-type': 'multipart/mixed' }), + })); - const link = new HttpLink({ - fetch: fetch as any, - }); + const link = new HttpLink({ + fetch: fetch as any, + }); - let i = 0; - execute(link, { query: sampleDeferredQuery }).subscribe( - result => { - try { - if (i === 0) { - expect(result).toEqual({ - data: { - stub: { - id: "0", - }, - }, - hasNext: true, - }); - } else if (i === 1) { - expect(result).toEqual({ - incremental: [{ + let i = 0; + execute(link, { query: sampleDeferredQuery }).subscribe( + result => { + try { + if (i === 0) { + expect(result).toEqual({ data: { - name: 'stubby', + stub: { + id: "0", + }, }, - extensions: { - timestamp: 1633038919, - }, - path: ['stub'], - }], - hasNext: false, - }); - } + hasNext: true, + }); + } else if (i === 1) { + expect(result).toEqual({ + incremental: [{ + data: { + name: 'stubby', + }, + extensions: { + timestamp: 1633038919, + }, + path: ['stub'], + }], + hasNext: false, + }); + } - } catch (err) { + } catch (err) { + done(err); + } finally { + i++; + } + }, + err => { done(err); - } finally { - i++; - } - }, - err => { - done(err); - }, - () => { - if (i !== 2) { - done(new Error("Unexpected end to observable")); - } + }, + () => { + if (i !== 2) { + done(new Error("Unexpected end to observable")); + } - done(); - }, - ); - }); + done(); + }, + ); + }); - it('can handle node stream bodies', (done) => { - const stream = Readable.from(body.split("\r\n").map((line) => line + "\r\n")); + it('can handle node stream bodies', (done) => { + const stream = Readable.from(body.split("\r\n").map((line) => line + "\r\n")); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ 'Content-Type': 'multipart/mixed;boundary="-";deferSpec=20220824' }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ 'Content-Type': 'multipart/mixed;boundary="-";deferSpec=20220824' }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); - let i = 0; - execute(link, { query: sampleDeferredQuery }).subscribe( - result => { - try { - if (i === 0) { - expect(result).toEqual({ - data: { - stub: { - id: "0", - }, - }, - hasNext: true, - }); - } else if (i === 1) { - expect(result).toEqual({ - incremental: [{ + let i = 0; + execute(link, { query: sampleDeferredQuery }).subscribe( + result => { + try { + if (i === 0) { + expect(result).toEqual({ data: { - name: 'stubby', + stub: { + id: "0", + }, }, - extensions: { - timestamp: 1633038919, - }, - path: ['stub'], - }], - hasNext: false, - }); - } + hasNext: true, + }); + } else if (i === 1) { + expect(result).toEqual({ + incremental: [{ + data: { + name: 'stubby', + }, + extensions: { + timestamp: 1633038919, + }, + path: ['stub'], + }], + hasNext: false, + }); + } - } catch (err) { + } catch (err) { + done(err); + } finally { + i++; + } + }, + err => { done(err); - } finally { - i++; - } - }, - err => { - done(err); - }, - () => { - if (i !== 2) { - done(new Error("Unexpected end to observable")); - } + }, + () => { + if (i !== 2) { + done(new Error("Unexpected end to observable")); + } - done(); - }, - ); - }); + done(); + }, + ); + }); - itAsync('sets correct accept header on request with deferred query', (resolve, reject) => { - const stream = Readable.from(body.split("\r\n").map((line) => line + "\r\n")); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ 'Content-Type': 'multipart/mixed' }), - })); - const link = new HttpLink({ - fetch: fetch as any, + itAsync('sets correct accept header on request with deferred query', (resolve, reject) => { + const stream = Readable.from(body.split("\r\n").map((line) => line + "\r\n")); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ 'Content-Type': 'multipart/mixed' }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + execute(link, { + query: sampleDeferredQuery + }).subscribe( + makeCallback(resolve, reject, () => { + expect(fetch).toHaveBeenCalledWith( + '/graphql', + expect.objectContaining({ + headers: { + "content-type": "application/json", + accept: "multipart/mixed; deferSpec=20220824, application/json" + } + }) + ) + }), + ); }); - execute(link, { - query: sampleDeferredQuery - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - '/graphql', - expect.objectContaining({ - headers: { - "content-type": "application/json", - accept: "multipart/mixed; deferSpec=20220824, application/json" - } - }) - ) - }), - ); - }); - // ensure that custom directives beginning with '@defer..' do not trigger - // custom accept header for multipart responses - itAsync('sets does not set accept header on query with custom directive begging with @defer', (resolve, reject) => { - const stream = Readable.from(body.split("\r\n").map((line) => line + "\r\n")); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ 'Content-Type': 'multipart/mixed' }), - })); - const link = new HttpLink({ - fetch: fetch as any, + // ensure that custom directives beginning with '@defer..' do not trigger + // custom accept header for multipart responses + itAsync('sets does not set accept header on query with custom directive begging with @defer', (resolve, reject) => { + const stream = Readable.from(body.split("\r\n").map((line) => line + "\r\n")); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ 'Content-Type': 'multipart/mixed' }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + execute(link, { + query: sampleQueryCustomDirective + }).subscribe( + makeCallback(resolve, reject, () => { + expect(fetch).toHaveBeenCalledWith( + '/graphql', + expect.objectContaining({ + headers: { + accept: "*/*", + "content-type": "application/json", + } + }) + ) + }), + ); }); - execute(link, { - query: sampleQueryCustomDirective - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - '/graphql', - expect.objectContaining({ - headers: { - accept: "*/*", - "content-type": "application/json", + }); + + describe('subscriptions', () => { + const subscribtionsBody = [ + '---', + 'Content-Type: application/json', + '', + '{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Subscription.aNewDieWasCreated","path":[]}]}}', + '---', + 'Content-Type: application/json', + '', + '{"data":{"aNewDieWasCreated":{"die":{"roll":153,"sides":191,"color":"blue"}}}}', + '---', + 'Content-Type: application/json', + '', + '{"data":{"aNewDieWasCreated":{"die":{"roll":107,"sides":154,"color":"red"}}}}', + '-----', + ].join("\r\n"); + + it('subscriptionsmultipart whatwg stream bodies', (done) => { + const stream = new ReadableStream({ + async start(controller) { + const lines = subscribtionsBody.split("\r\n"); + try { + for (const line of lines) { + await new Promise((resolve) => setTimeout(resolve, 10)); + controller.enqueue(line + "\r\n"); } - }) - ) - }), - ); + } finally { + controller.close(); + } + }, + }); + + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ 'content-type': 'multipart/mixed' }), + })); + + const link = new HttpLink({ + fetch: fetch as any, + }); + + let i = 0; + execute(link, { query: sampleDeferredQuery }).subscribe( + result => { + try { + if (i === 0) { + expect(result).toEqual({ + data: null, + extensions: { + valueCompletion: [ + { + message: "Cannot return null for non-nullable field Subscription.aNewDieWasCreated", + path: [] + } + ] + } + }); + } else if (i === 1) { + expect(result).toEqual({ + data: { + aNewDieWasCreated: { + die: { + color: 'blue', + roll: 153, + sides: 191 + } + } + } + }); + } else if (i === 2) { + console.log(result); + expect(result).toEqual({ + data: { + aNewDieWasCreated: { + die: { + color: 'red', + roll: 107, + sides: 154 + } + } + } + }); + } + } catch (err) { + done(err); + } finally { + i++; + } + }, + err => { + done(err); + }, + () => { + if (i !== 3) { + done(new Error("Unexpected end to observable")); + } + done(); + }, + ); + }); }); }); }); diff --git a/src/link/http/createHttpLink.ts b/src/link/http/createHttpLink.ts index 4022d2ef661..e9a340668c7 100644 --- a/src/link/http/createHttpLink.ts +++ b/src/link/http/createHttpLink.ts @@ -129,6 +129,9 @@ export const createHttpLink = (linkOptions: HttpOptions = {}) => { const definitionIsMutation = (d: DefinitionNode) => { return d.kind === 'OperationDefinition' && d.operation === 'mutation'; }; + const definitionIsSubscription = (d: DefinitionNode) => { + return d.kind === 'OperationDefinition' && d.operation === 'subscription'; + }; if ( useGETForQueries && !operation.query.definitions.some(definitionIsMutation) @@ -137,7 +140,10 @@ export const createHttpLink = (linkOptions: HttpOptions = {}) => { } // does not match custom directives beginning with @defer - if (hasDirectives(['defer'], operation.query)) { + if ( + hasDirectives(['defer'], operation.query) || + operation.query.definitions.some(definitionIsSubscription) + ) { options.headers = options.headers || {}; options.headers.accept = "multipart/mixed; deferSpec=20220824, application/json"; }