From ab42a5c08840193cb915f4e66d71fac3834fec68 Mon Sep 17 00:00:00 2001 From: Marcel Laverdet Date: Thu, 18 May 2023 15:06:53 -0400 Subject: [PATCH] Fix `@defer` with payload containing "---" (#10891) * Fix `@defer` with payload containing "---" When using `@defer` if the response payload contains "---" then the query will fail with an error message such as: ApolloError: Unterminated string in JSON at position 15378 * chore(tests): updates HttpLink test to validate that boundaries are not parsed within response data, only read at the beginning of each chunk. * chore: update changeset --------- Co-authored-by: alessia --- .changeset/angry-weeks-marry.md | 5 ++++ src/link/http/__tests__/HttpLink.ts | 9 ++++-- src/link/http/parseAndCheckHttpResponse.ts | 35 ++++++++++++---------- 3 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 .changeset/angry-weeks-marry.md diff --git a/.changeset/angry-weeks-marry.md b/.changeset/angry-weeks-marry.md new file mode 100644 index 00000000000..927a0459b82 --- /dev/null +++ b/.changeset/angry-weeks-marry.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fixes a bug in how multipart responses are read when using `@defer`. When reading a multipart body, `HttpLink` no longer attempts to parse the boundary (e.g. `"---"` or other boundary string) within the response data itself, only when reading the beginning of each mulitpart chunked message. diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index c72e254f809..ae9fa3a36d2 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -1360,7 +1360,10 @@ describe('HttpLink', () => { 'Content-Type: application/json; charset=utf-8', 'Content-Length: 58', '', - '{"hasNext":false, "incremental": [{"data":{"name":"stubby"},"path":["stub"],"extensions":{"timestamp":1633038919}}]}', + // Intentionally using the boundary value `---` within the “name” to + // validate that boundary delimiters are not parsed within the response + // data itself, only read at the beginning of each chunk. + '{"hasNext":false, "incremental": [{"data":{"name":"stubby---"},"path":["stub"],"extensions":{"timestamp":1633038919}}]}', '-----', ].join("\r\n"); @@ -1418,7 +1421,7 @@ describe('HttpLink', () => { expect(result).toEqual({ incremental: [{ data: { - name: 'stubby', + name: 'stubby---', }, extensions: { timestamp: 1633038919, @@ -1544,7 +1547,7 @@ describe('HttpLink', () => { expect(result).toEqual({ incremental: [{ data: { - name: 'stubby', + name: 'stubby---', }, extensions: { timestamp: 1633038919, diff --git a/src/link/http/parseAndCheckHttpResponse.ts b/src/link/http/parseAndCheckHttpResponse.ts index ba9307ac9e2..134700f58b0 100644 --- a/src/link/http/parseAndCheckHttpResponse.ts +++ b/src/link/http/parseAndCheckHttpResponse.ts @@ -39,7 +39,7 @@ export async function readMultipartBody< .trim() : "-"; - let boundary = `--${boundaryVal}`; + const boundary = `\r\n--${boundaryVal}`; let buffer = ""; const iterator = responseIterator(response); let running = true; @@ -47,9 +47,10 @@ export async function readMultipartBody< while (running) { const { value, done } = await iterator.next(); const chunk = typeof value === "string" ? value : decoder.decode(value); + const searchFrom = buffer.length - boundary.length + 1; running = !done; buffer += chunk; - let bi = buffer.indexOf(boundary); + let bi = buffer.indexOf(boundary, searchFrom); while (bi > -1) { let message: string; @@ -57,22 +58,24 @@ export async function readMultipartBody< buffer.slice(0, bi), buffer.slice(bi + boundary.length), ]; - if (message.trim()) { - const i = message.indexOf("\r\n\r\n"); - const headers = parseHeaders(message.slice(0, i)); - const contentType = headers["content-type"]; - if ( - contentType && - contentType.toLowerCase().indexOf("application/json") === -1 - ) { - throw new Error( - "Unsupported patch content type: application/json is required." - ); - } - const body = message.slice(i); + const i = message.indexOf("\r\n\r\n"); + const headers = parseHeaders(message.slice(0, i)); + const contentType = headers["content-type"]; + if ( + contentType && + contentType.toLowerCase().indexOf("application/json") === -1 + ) { + throw new Error( + "Unsupported patch content type: application/json is required." + ); + } + // nb: Technically you'd want to slice off the beginning "\r\n" but since + // this is going to be `JSON.parse`d there is no need. + const body = message.slice(i); + if (body) { try { - const result = parseJsonBody(response, body.replace("\r\n", "")); + const result = parseJsonBody(response, body); if ( Object.keys(result).length > 1 || "data" in result ||