var setCookie = {exports: {}}; var defaultParseOptions = { decodeValues: true, map: false, silent: false, }; function isNonEmptyString(str) { return typeof str === "string" && !!str.trim(); } function parseString(setCookieValue, options) { var parts = setCookieValue.split(";").filter(isNonEmptyString); var nameValue = parts.shift().split("="); var name = nameValue.shift(); var value = nameValue.join("="); // everything after the first =, joined by a "=" if there was more than one part options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions; try { value = options.decodeValues ? decodeURIComponent(value) : value; // decode cookie value } catch (e) { console.error( "set-cookie-parser encountered an error while decoding a cookie with value '" + value + "'. Set options.decodeValues to false to disable this feature.", e ); } var cookie = { name: name, // grab everything before the first = value: value, }; parts.forEach(function (part) { var sides = part.split("="); var key = sides.shift().trimLeft().toLowerCase(); var value = sides.join("="); if (key === "expires") { cookie.expires = new Date(value); } else if (key === "max-age") { cookie.maxAge = parseInt(value, 10); } else if (key === "secure") { cookie.secure = true; } else if (key === "httponly") { cookie.httpOnly = true; } else if (key === "samesite") { cookie.sameSite = value; } else { cookie[key] = value; } }); return cookie; } function parse(input, options) { options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions; if (!input) { if (!options.map) { return []; } else { return {}; } } if (input.headers && input.headers["set-cookie"]) { // fast-path for node.js (which automatically normalizes header names to lower-case input = input.headers["set-cookie"]; } else if (input.headers) { // slow-path for other environments - see #25 var sch = input.headers[ Object.keys(input.headers).find(function (key) { return key.toLowerCase() === "set-cookie"; }) ]; // warn if called on a request-like object with a cookie header rather than a set-cookie header - see #34, 36 if (!sch && input.headers.cookie && !options.silent) { console.warn( "Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning." ); } input = sch; } if (!Array.isArray(input)) { input = [input]; } options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions; if (!options.map) { return input.filter(isNonEmptyString).map(function (str) { return parseString(str, options); }); } else { var cookies = {}; return input.filter(isNonEmptyString).reduce(function (cookies, str) { var cookie = parseString(str, options); cookies[cookie.name] = cookie; return cookies; }, cookies); } } /* Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas that are within a single set-cookie field-value, such as in the Expires portion. This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 React Native's fetch does this for *every* header, including set-cookie. Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation */ function splitCookiesString(cookiesString) { if (Array.isArray(cookiesString)) { return cookiesString; } if (typeof cookiesString !== "string") { return []; } var cookiesStrings = []; var pos = 0; var start; var ch; var lastComma; var nextStart; var cookiesSeparatorFound; function skipWhitespace() { while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { pos += 1; } return pos < cookiesString.length; } function notSpecialChar() { ch = cookiesString.charAt(pos); return ch !== "=" && ch !== ";" && ch !== ","; } while (pos < cookiesString.length) { start = pos; cookiesSeparatorFound = false; while (skipWhitespace()) { ch = cookiesString.charAt(pos); if (ch === ",") { // ',' is a cookie separator if we have later first '=', not ';' or ',' lastComma = pos; pos += 1; skipWhitespace(); nextStart = pos; while (pos < cookiesString.length && notSpecialChar()) { pos += 1; } // currently special character if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { // we found cookies separator cookiesSeparatorFound = true; // pos is inside the next cookie, so back up and return it. pos = nextStart; cookiesStrings.push(cookiesString.substring(start, lastComma)); start = pos; } else { // in param ',' or param separator ';', // we continue from that comma pos = lastComma + 1; } } else { pos += 1; } } if (!cookiesSeparatorFound || pos >= cookiesString.length) { cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); } } return cookiesStrings; } setCookie.exports = parse; setCookie.exports.parse = parse; setCookie.exports.parseString = parseString; var splitCookiesString_1 = setCookie.exports.splitCookiesString = splitCookiesString; /** @param {import('http').IncomingMessage} req */ function get_raw_body(req) { const h = req.headers; if (!h['content-type']) { return null; } const length = Number(h['content-length']); // check if no request body // https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95 if (isNaN(length) && h['transfer-encoding'] == null) { return null; } 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) => { controller.error(error); }); req.on('end', () => { if (cancelled) return; controller.close(); }); req.on('data', (chunk) => { if (cancelled) return; size += chunk.length; if (size > length) { controller.error(new Error('content-length exceeded')); return; } controller.enqueue(chunk); if (controller.desiredSize === null || controller.desiredSize <= 0) { req.pause(); } }); }, pull() { req.resume(); }, cancel(reason) { cancelled = true; req.destroy(reason); } }); } /** @type {import('@sveltejs/kit/node').getRequest} */ async function getRequest(base, req) { let headers = /** @type {Record} */ (req.headers); if (req.httpVersionMajor === 2) { // we need to strip out the HTTP/2 pseudo-headers because node-fetch's // Request implementation doesn't like them // TODO is this still true with Node 18 headers = Object.assign({}, headers); delete headers[':method']; delete headers[':path']; delete headers[':authority']; delete headers[':scheme']; } return new Request(base + req.url, { method: req.method, headers, body: get_raw_body(req) }); } /** @type {import('@sveltejs/kit/node').setResponse} */ async function setResponse(res, response) { const headers = Object.fromEntries(response.headers); if (response.headers.has('set-cookie')) { const header = /** @type {string} */ (response.headers.get('set-cookie')); const split = splitCookiesString_1(header); // @ts-expect-error headers['set-cookie'] = split; } res.writeHead(response.status, headers); if (!response.body) { res.end(); return; } const reader = response.body.getReader(); if (res.destroyed) { reader.cancel(); return; } const cancel = (/** @type {Error|undefined} */ 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))); } } } export { getRequest, setResponse };