@astrojs/vercel
Advanced tools
Comparing version 0.0.0-set-cookie-20221202105120 to 0.0.0-set-cookie-20221202112631
# @astrojs/vercel | ||
## 0.0.0-set-cookie-20221202105120 | ||
## 0.0.0-set-cookie-20221202112631 | ||
@@ -5,0 +5,0 @@ ### Patch Changes |
/// <reference types="node" /> | ||
import type { App } from 'astro/app'; | ||
import type { IncomingMessage, ServerResponse } from 'node:http'; | ||
export declare function getRequest(base: string, req: IncomingMessage): Promise<Request>; | ||
export declare function getRequest(base: string, req: IncomingMessage, bodySizeLimit?: number): Promise<Request>; | ||
export declare function setResponse(app: App, res: ServerResponse, response: Response): Promise<void>; |
@@ -1,42 +0,69 @@ | ||
import { Readable } from "node:stream"; | ||
import { splitCookiesString } from "set-cookie-parser"; | ||
const clientAddressSymbol = Symbol.for("astro.clientAddress"); | ||
function get_raw_body(req) { | ||
return new Promise((fulfil, reject) => { | ||
const h = req.headers; | ||
if (!h["content-type"]) { | ||
return fulfil(null); | ||
function get_raw_body(req, body_size_limit) { | ||
const h = req.headers; | ||
if (!h["content-type"]) { | ||
return null; | ||
} | ||
const content_length = Number(h["content-length"]); | ||
if (req.httpVersionMajor === 1 && isNaN(content_length) && h["transfer-encoding"] == null || content_length === 0) { | ||
return 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.` | ||
); | ||
} | ||
req.on("error", reject); | ||
const length = Number(h["content-length"]); | ||
if (isNaN(length) && h["transfer-encoding"] == null) { | ||
return fulfil(null); | ||
} | ||
let data = new Uint8Array(length || 0); | ||
if (length > 0) { | ||
let offset = 0; | ||
} | ||
if (req.destroyed) { | ||
const readable = new ReadableStream(); | ||
readable.cancel(); | ||
return readable; | ||
} | ||
let size = 0; | ||
let cancelled = false; | ||
return new ReadableStream({ | ||
start(controller) { | ||
req.on("error", (error) => { | ||
cancelled = true; | ||
controller.error(error); | ||
}); | ||
req.on("end", () => { | ||
if (cancelled) | ||
return; | ||
controller.close(); | ||
}); | ||
req.on("data", (chunk) => { | ||
const new_len = offset + Buffer.byteLength(chunk); | ||
if (new_len > length) { | ||
return reject({ | ||
status: 413, | ||
reason: 'Exceeded "Content-Length" limit' | ||
}); | ||
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; | ||
} | ||
data.set(chunk, offset); | ||
offset = new_len; | ||
controller.enqueue(chunk); | ||
if (controller.desiredSize === null || controller.desiredSize <= 0) { | ||
req.pause(); | ||
} | ||
}); | ||
} 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; | ||
}); | ||
}, | ||
pull() { | ||
req.resume(); | ||
}, | ||
cancel(reason) { | ||
cancelled = true; | ||
req.destroy(reason); | ||
} | ||
req.on("end", () => { | ||
fulfil(data); | ||
}); | ||
}); | ||
} | ||
async function getRequest(base, req) { | ||
async function getRequest(base, req, bodySizeLimit) { | ||
let headers = req.headers; | ||
@@ -53,3 +80,3 @@ if (req.httpVersionMajor === 2) { | ||
headers, | ||
body: await get_raw_body(req) | ||
body: get_raw_body(req, bodySizeLimit) | ||
}); | ||
@@ -61,29 +88,56 @@ Reflect.set(request, clientAddressSymbol, headers["x-forwarded-for"]); | ||
const headers = Object.fromEntries(response.headers); | ||
let setCookie = []; | ||
let cookies = []; | ||
if (response.headers.has("set-cookie")) { | ||
if ("raw" in response.headers) { | ||
const rawPacked = response.headers.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; | ||
} | ||
if (app.setCookieHeaders) { | ||
const setCookieHeaders = Array.from(app.setCookieHeaders(response)); | ||
setCookie.push(...setCookieHeaders); | ||
cookies.push(...setCookieHeaders); | ||
} | ||
res.writeHead(response.status, { | ||
...headers, | ||
"Set-Cookie": setCookie | ||
}); | ||
if (response.body instanceof Readable) { | ||
response.body.pipe(res); | ||
} else { | ||
if (response.body) { | ||
res.write(await response.arrayBuffer()); | ||
} | ||
res.writeHead(response.status, { ...headers, "set-cookie": cookies }); | ||
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) => { | ||
res.off("close", cancel); | ||
res.off("error", cancel); | ||
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))); | ||
} | ||
} | ||
} | ||
@@ -90,0 +144,0 @@ export { |
{ | ||
"name": "@astrojs/vercel", | ||
"description": "Deploy your site to Vercel", | ||
"version": "0.0.0-set-cookie-20221202105120", | ||
"version": "0.0.0-set-cookie-20221202112631", | ||
"type": "module", | ||
@@ -43,6 +43,8 @@ "author": "withastro", | ||
"@vercel/nft": "^0.22.1", | ||
"fast-glob": "^3.2.11" | ||
"fast-glob": "^3.2.11", | ||
"set-cookie-parser": "^2.5.1" | ||
}, | ||
"devDependencies": { | ||
"astro": "0.0.0-set-cookie-20221202105120", | ||
"@types/set-cookie-parser": "^2.4.2", | ||
"astro": "0.0.0-set-cookie-20221202112631", | ||
"astro-scripts": "0.0.9", | ||
@@ -49,0 +51,0 @@ "chai": "^4.3.6", |
import type { App } from 'astro/app'; | ||
import type { IncomingMessage, ServerResponse } from 'node:http'; | ||
import { Readable } from 'node:stream'; | ||
import { splitCookiesString } from 'set-cookie-parser'; | ||
@@ -9,55 +9,95 @@ 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<Uint8Array | null>((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; | ||
let size = 0; | ||
let cancelled = false; | ||
return new ReadableStream({ | ||
start(controller) { | ||
req.on('error', (error) => { | ||
cancelled = true; | ||
controller.error(error); | ||
}); | ||
req.on('end', () => { | ||
if (cancelled) return; | ||
controller.close(); | ||
}); | ||
req.on('data', (chunk) => { | ||
const new_len = offset + Buffer.byteLength(chunk); | ||
if (cancelled) return; | ||
if (new_len > length) { | ||
return reject({ | ||
status: 413, | ||
reason: 'Exceeded "Content-Length" limit', | ||
}); | ||
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; | ||
} | ||
data.set(chunk, offset); | ||
offset = new_len; | ||
controller.enqueue(chunk); | ||
if (controller.desiredSize === null || controller.desiredSize <= 0) { | ||
req.pause(); | ||
} | ||
}); | ||
} 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; | ||
}); | ||
} | ||
}, | ||
req.on('end', () => { | ||
fulfil(data); | ||
}); | ||
pull() { | ||
req.resume(); | ||
}, | ||
cancel(reason) { | ||
cancelled = true; | ||
req.destroy(reason); | ||
}, | ||
}); | ||
} | ||
export async function getRequest(base: string, req: IncomingMessage): Promise<Request> { | ||
export async function getRequest( | ||
base: string, | ||
req: IncomingMessage, | ||
bodySizeLimit?: number | ||
): Promise<Request> { | ||
let headers = req.headers as Record<string, string>; | ||
@@ -76,3 +116,3 @@ if (req.httpVersionMajor === 2) { | ||
headers, | ||
body: await get_raw_body(req), // TODO stream rather than buffer | ||
body: get_raw_body(req, bodySizeLimit), | ||
}); | ||
@@ -83,52 +123,71 @@ Reflect.set(request, clientAddressSymbol, headers['x-forwarded-for']); | ||
export async function setResponse( | ||
app: App, | ||
res: ServerResponse, | ||
response: Response | ||
): Promise<void> { | ||
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<string, string[]>; | ||
}; | ||
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))); | ||
} | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
74016
1407
4
5
+ Addedset-cookie-parser@^2.5.1
+ Addedset-cookie-parser@2.7.1(transitive)