diff options
author | 2023-06-06 11:09:16 -0400 | |
---|---|---|
committer | 2023-06-06 10:09:16 -0500 | |
commit | 4929332c3210d1634b8607c7736d9049860a2079 (patch) | |
tree | f232675fb2d6820e6262a0f50aaa49efa42e9e0e /packages/integrations/node/src | |
parent | 409c60028aaab09b8f2383ef5730531cd23db4ba (diff) | |
download | astro-4929332c3210d1634b8607c7736d9049860a2079.tar.gz astro-4929332c3210d1634b8607c7736d9049860a2079.tar.zst astro-4929332c3210d1634b8607c7736d9049860a2079.zip |
#7226 - fixes NodeJS adapter for multiple set-cookie headers (and other header issues) (#7227)
* Utilizes the new standard WebAPI Fetch Headers.getSetCookie() function
to safely handle multiple set-cookie headers when converting from a
WebAPI Response to a NodeJS ServerResponse
Modifies the existing nodeMiddleware logic which first set AstroCookies
on ServerResponse.setHeader(...) and then called
ServerResponse.writeHead(status, Response.headers) which means any that
if the WebAPI Response had any set-cookie headers on it, they would
replace anything from AstroCookies.
The new logic delegates appending AstroCookie values onto the WebAPI
Response Headers object, so that a single unified function safely
converts the WebAPI Response Headers into a NodeJS compatible
OutgoingHttpHeaders object utilizing the new standard
Headers.getSetCookie() function provided by the undici WebAPI polyfills.
Plus extensive test coverage.
* #7226 - changeset for NodeJS adapter set-cookie fix
* fixing all double quotes to single quotes
---------
Co-authored-by: Alex Sherwin <alex.sherwin@acadia.inc>
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Diffstat (limited to 'packages/integrations/node/src')
-rw-r--r-- | packages/integrations/node/src/createOutgoingHttpHeaders.ts | 40 | ||||
-rw-r--r-- | packages/integrations/node/src/nodeMiddleware.ts | 9 |
2 files changed, 47 insertions, 2 deletions
diff --git a/packages/integrations/node/src/createOutgoingHttpHeaders.ts b/packages/integrations/node/src/createOutgoingHttpHeaders.ts new file mode 100644 index 000000000..5b99cfa4f --- /dev/null +++ b/packages/integrations/node/src/createOutgoingHttpHeaders.ts @@ -0,0 +1,40 @@ +import type { OutgoingHttpHeaders } from 'http'; + +/** + * Takes in a nullable WebAPI Headers object and produces a NodeJS OutgoingHttpHeaders object suitable for usage + * with ServerResponse.writeHead(..) or ServerResponse.setHeader(..) + * + * @param webHeaders WebAPI Headers object + * @returns NodeJS OutgoingHttpHeaders object with multiple set-cookie handled as an array of values + */ +export const createOutgoingHttpHeaders = (webHeaders: Headers | undefined | null): OutgoingHttpHeaders | undefined => { + if (!webHeaders) { + return undefined; + } + + // re-type to access Header.getSetCookie() + const headers = webHeaders as HeadersWithGetSetCookie; + + // at this point, a multi-value'd set-cookie header is invalid (it was concatenated as a single CSV, which is not valid for set-cookie) + const nodeHeaders: OutgoingHttpHeaders = Object.fromEntries(headers.entries()); + + if (Object.keys(nodeHeaders).length === 0) { + return undefined; + } + + // if there is > 1 set-cookie header, we have to fix it to be an array of values + if (headers.has('set-cookie')) { + const cookieHeaders = headers.getSetCookie(); + if (cookieHeaders.length > 1) { + // the Headers.entries() API already normalized all header names to lower case so we can safely index this as 'set-cookie' + nodeHeaders['set-cookie'] = cookieHeaders; + } + } + + return nodeHeaders; +}; + +interface HeadersWithGetSetCookie extends Headers { + // the @astrojs/webapi polyfill makes this available (as of undici@5.19.0), but tsc doesn't pick it up on the built-in Headers type from DOM lib + getSetCookie(): string[]; +} diff --git a/packages/integrations/node/src/nodeMiddleware.ts b/packages/integrations/node/src/nodeMiddleware.ts index c23cdb89c..492130daa 100644 --- a/packages/integrations/node/src/nodeMiddleware.ts +++ b/packages/integrations/node/src/nodeMiddleware.ts @@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; import type { Readable } from 'stream'; import { responseIterator } from './response-iterator'; import type { Options } from './types'; +import { createOutgoingHttpHeaders } from './createOutgoingHttpHeaders'; export default function (app: NodeApp, mode: Options['mode']) { return async function ( @@ -44,12 +45,16 @@ async function writeWebResponse(app: NodeApp, res: ServerResponse, webResponse: if (app.setCookieHeaders) { const setCookieHeaders: Array<string> = Array.from(app.setCookieHeaders(webResponse)); + if (setCookieHeaders.length) { - res.setHeader('Set-Cookie', setCookieHeaders); + for (const setCookieHeader of setCookieHeaders) { + webResponse.headers.append('set-cookie', setCookieHeader); + } } } - res.writeHead(status, Object.fromEntries(headers.entries())); + const nodeHeaders = createOutgoingHttpHeaders(headers); + res.writeHead(status, nodeHeaders); if (webResponse.body) { try { for await (const chunk of responseIterator(webResponse) as unknown as Readable) { |