From 9fb3a8040aae9cda74a60df36b3bacdfdfd822de Mon Sep 17 00:00:00 2001 From: JuanM04 Date: Fri, 2 Dec 2022 07:47:31 -0300 Subject: [PATCH 1/3] fix(vercel): Update set-cookie header hanlder --- .changeset/plenty-tigers-pretend.md | 5 +++ .../src/serverless/request-transform.ts | 34 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 .changeset/plenty-tigers-pretend.md diff --git a/.changeset/plenty-tigers-pretend.md b/.changeset/plenty-tigers-pretend.md new file mode 100644 index 000000000000..95aad2fe99d0 --- /dev/null +++ b/.changeset/plenty-tigers-pretend.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': patch +--- + +Update set-cookie header hanlder diff --git a/packages/integrations/vercel/src/serverless/request-transform.ts b/packages/integrations/vercel/src/serverless/request-transform.ts index 7212431c72a0..c51d0c113cf3 100644 --- a/packages/integrations/vercel/src/serverless/request-transform.ts +++ b/packages/integrations/vercel/src/serverless/request-transform.ts @@ -84,20 +84,40 @@ export async function setResponse( response: Response ): Promise { const headers = Object.fromEntries(response.headers); + let setCookie: string[] = []; if (response.headers.has('set-cookie')) { - // @ts-expect-error (headers.raw() is non-standard) - headers['set-cookie'] = response.headers.raw()['set-cookie']; + // Special-case set-cookie which has to be set an different way :/ + // The fetch API does not have a way to get multiples of a single header, but instead concatenates + // them. There are non-standard ways to do it, and node-fetch gives us headers.raw() + // See https://github.com/whatwg/fetch/issues/973 for discussion + if ('raw' in response.headers) { + // Node fetch allows you to get the raw headers, which includes multiples of the same type. + // This is needed because Set-Cookie *must* be called for each cookie, and can't be + // concatenated together. + type HeadersWithRaw = Headers & { + raw: () => Record; + }; + + const rawPacked = (response.headers as HeadersWithRaw).raw(); + if ('set-cookie' in rawPacked && setCookie.length === 0) { + setCookie = rawPacked['set-cookie']; + } + } else { + setCookie = [response.headers.get('set-cookie')!]; + } } + // Apply cookies set via Astro.cookies.set/delete if (app.setCookieHeaders) { - const setCookieHeaders: Array = Array.from(app.setCookieHeaders(response)); - if (setCookieHeaders.length) { - res.setHeader('Set-Cookie', setCookieHeaders); - } + const setCookieHeaders = Array.from(app.setCookieHeaders(response)); + setCookie.push(...setCookieHeaders); } - res.writeHead(response.status, headers); + res.writeHead(response.status, { + ...headers, + 'Set-Cookie': setCookie, + }); if (response.body instanceof Readable) { response.body.pipe(res); From c6709c5a4585f8712dbcd8a545f114ce3ed66d89 Mon Sep 17 00:00:00 2001 From: JuanM04 Date: Fri, 2 Dec 2022 08:23:45 -0300 Subject: [PATCH 2/3] Update from SvelteKit adapter --- packages/integrations/vercel/package.json | 4 +- .../src/serverless/request-transform.ts | 207 +++++++++++------- pnpm-lock.yaml | 11 +- 3 files changed, 146 insertions(+), 76 deletions(-) diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 7827f2dca3ae..9f15ccf639fc 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -46,9 +46,11 @@ "dependencies": { "@astrojs/webapi": "^1.1.1", "@vercel/nft": "^0.22.1", - "fast-glob": "^3.2.11" + "fast-glob": "^3.2.11", + "set-cookie-parser": "^2.5.1" }, "devDependencies": { + "@types/set-cookie-parser": "^2.4.2", "astro": "workspace:*", "astro-scripts": "workspace:*", "chai": "^4.3.6", diff --git a/packages/integrations/vercel/src/serverless/request-transform.ts b/packages/integrations/vercel/src/serverless/request-transform.ts index c51d0c113cf3..a42bac1f58ad 100644 --- a/packages/integrations/vercel/src/serverless/request-transform.ts +++ b/packages/integrations/vercel/src/serverless/request-transform.ts @@ -1,64 +1,104 @@ import type { App } from 'astro/app'; import type { IncomingMessage, ServerResponse } from 'node:http'; -import { Readable } from 'node:stream'; +import { splitCookiesString } from 'set-cookie-parser'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); /* Credits to the SvelteKit team - https://github.com/sveltejs/kit/blob/69913e9fda054fa6a62a80e2bb4ee7dca1005796/packages/kit/src/node.js + https://github.com/sveltejs/kit/blob/dd380b38c322272b414a7ec3ac2911f2db353f5c/packages/kit/src/exports/node/index.js */ -function get_raw_body(req: IncomingMessage) { - return new Promise((fulfil, reject) => { - const h = req.headers; +function get_raw_body(req: IncomingMessage, body_size_limit?: number): ReadableStream | null { + const h = req.headers; - if (!h['content-type']) { - return fulfil(null); - } + if (!h['content-type']) { + return null; + } - req.on('error', reject); + const content_length = Number(h['content-length']); - const length = Number(h['content-length']); + // check if no request body + if ( + (req.httpVersionMajor === 1 && isNaN(content_length) && h['transfer-encoding'] == null) || + content_length === 0 + ) { + return null; + } - // https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95 - if (isNaN(length) && h['transfer-encoding'] == null) { - return fulfil(null); + let length = content_length; + + if (body_size_limit) { + if (!length) { + length = body_size_limit; + } else if (length > body_size_limit) { + throw new Error( + `Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.` + ); } + } - let data = new Uint8Array(length || 0); + if (req.destroyed) { + const readable = new ReadableStream(); + readable.cancel(); + return readable; + } - if (length > 0) { - let offset = 0; - req.on('data', (chunk) => { - const new_len = offset + Buffer.byteLength(chunk); + let size = 0; + let cancelled = false; - if (new_len > length) { - return reject({ - status: 413, - reason: 'Exceeded "Content-Length" limit', - }); - } + return new ReadableStream({ + start(controller) { + req.on('error', (error) => { + cancelled = true; + controller.error(error); + }); - data.set(chunk, offset); - offset = new_len; + req.on('end', () => { + if (cancelled) return; + controller.close(); }); - } else { + req.on('data', (chunk) => { - const new_data = new Uint8Array(data.length + chunk.length); - new_data.set(data, 0); - new_data.set(chunk, data.length); - data = new_data; + if (cancelled) return; + + size += chunk.length; + if (size > length) { + cancelled = true; + controller.error( + new Error( + `request body size exceeded ${ + content_length ? "'content-length'" : 'BODY_SIZE_LIMIT' + } of ${length}` + ) + ); + return; + } + + controller.enqueue(chunk); + + if (controller.desiredSize === null || controller.desiredSize <= 0) { + req.pause(); + } }); - } + }, + + pull() { + req.resume(); + }, - req.on('end', () => { - fulfil(data); - }); + cancel(reason) { + cancelled = true; + req.destroy(reason); + }, }); } -export async function getRequest(base: string, req: IncomingMessage): Promise { +export async function getRequest( + base: string, + req: IncomingMessage, + bodySizeLimit?: number +): Promise { let headers = req.headers as Record; if (req.httpVersionMajor === 2) { // we need to strip out the HTTP/2 pseudo-headers because node-fetch's @@ -72,60 +112,79 @@ export async function getRequest(base: string, req: IncomingMessage): Promise { +export async function setResponse(app: App, res: ServerResponse, response: Response) { const headers = Object.fromEntries(response.headers); - let setCookie: string[] = []; + let cookies: string[] = []; if (response.headers.has('set-cookie')) { - // Special-case set-cookie which has to be set an different way :/ - // The fetch API does not have a way to get multiples of a single header, but instead concatenates - // them. There are non-standard ways to do it, and node-fetch gives us headers.raw() - // See https://github.com/whatwg/fetch/issues/973 for discussion - if ('raw' in response.headers) { - // Node fetch allows you to get the raw headers, which includes multiples of the same type. - // This is needed because Set-Cookie *must* be called for each cookie, and can't be - // concatenated together. - type HeadersWithRaw = Headers & { - raw: () => Record; - }; - - const rawPacked = (response.headers as HeadersWithRaw).raw(); - if ('set-cookie' in rawPacked && setCookie.length === 0) { - setCookie = rawPacked['set-cookie']; - } - } else { - setCookie = [response.headers.get('set-cookie')!]; - } + const header = response.headers.get('set-cookie')!; + const split = splitCookiesString(header); + cookies = split; } - // Apply cookies set via Astro.cookies.set/delete if (app.setCookieHeaders) { const setCookieHeaders = Array.from(app.setCookieHeaders(response)); - setCookie.push(...setCookieHeaders); + cookies.push(...setCookieHeaders); } - res.writeHead(response.status, { - ...headers, - 'Set-Cookie': setCookie, - }); + res.writeHead(response.status, { ...headers, 'set-cookie': cookies }); - if (response.body instanceof Readable) { - response.body.pipe(res); - } else { - if (response.body) { - res.write(await response.arrayBuffer()); - } + if (!response.body) { + res.end(); + return; + } + if (response.body.locked) { + res.write( + 'Fatal error: Response body is locked. ' + + `This can happen when the response was already read (for example through 'response.json()' or 'response.text()').` + ); res.end(); + return; + } + + const reader = response.body.getReader(); + + if (res.destroyed) { + reader.cancel(); + return; + } + + const cancel = (error?: Error) => { + res.off('close', cancel); + res.off('error', cancel); + + // If the reader has already been interrupted with an error earlier, + // then it will appear here, it is useless, but it needs to be catch. + reader.cancel(error).catch(() => {}); + if (error) res.destroy(error); + }; + + res.on('close', cancel); + res.on('error', cancel); + + next(); + async function next() { + try { + for (;;) { + const { done, value } = await reader.read(); + + if (done) break; + + if (!res.write(value)) { + res.once('drain', next); + return; + } + } + res.end(); + } catch (error) { + cancel(error instanceof Error ? error : new Error(String(error))); + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a3ffd82055d..33a430fb2d22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3225,17 +3225,21 @@ importers: packages/integrations/vercel: specifiers: '@astrojs/webapi': ^1.1.1 + '@types/set-cookie-parser': ^2.4.2 '@vercel/nft': ^0.22.1 astro: workspace:* astro-scripts: workspace:* chai: ^4.3.6 fast-glob: ^3.2.11 mocha: ^9.2.2 + set-cookie-parser: ^2.5.1 dependencies: '@astrojs/webapi': link:../../webapi '@vercel/nft': 0.22.1 fast-glob: 3.2.12 + set-cookie-parser: 2.5.1 devDependencies: + '@types/set-cookie-parser': 2.4.2 astro: link:../../astro astro-scripts: link:../../../scripts chai: 4.3.7 @@ -9872,6 +9876,12 @@ packages: '@types/node': 18.11.9 dev: true + /@types/set-cookie-parser/2.4.2: + resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} + dependencies: + '@types/node': 18.11.9 + dev: true + /@types/sharp/0.30.5: resolution: {integrity: sha512-EhO29617AIBqxoVtpd1qdBanWpspk/kD2B6qTFRJ31Q23Rdf+DNU1xlHSwtqvwq1vgOqBwq1i38SX+HGCymIQg==} dependencies: @@ -16745,7 +16755,6 @@ packages: /set-cookie-parser/2.5.1: resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} - dev: true /setprototypeof/1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} From 1047fa89ce0ea1cc48537d55e7e24d1dd9d37890 Mon Sep 17 00:00:00 2001 From: JuanM04 Date: Fri, 2 Dec 2022 09:05:03 -0300 Subject: [PATCH 3/3] Updated changeset --- .changeset/plenty-tigers-pretend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/plenty-tigers-pretend.md b/.changeset/plenty-tigers-pretend.md index 95aad2fe99d0..8aa23220724a 100644 --- a/.changeset/plenty-tigers-pretend.md +++ b/.changeset/plenty-tigers-pretend.md @@ -2,4 +2,4 @@ '@astrojs/vercel': patch --- -Update set-cookie header hanlder +Updated request-transform methods