| function createBodyTooLargeError(maxRequestBodySize) { | ||
| return Object.assign(/* @__PURE__ */ new Error(`Request body exceeds the maximum allowed size of ${maxRequestBodySize} bytes.`), { | ||
| code: "ERR_BODY_TOO_LARGE", | ||
| statusCode: 413, | ||
| status: 413 | ||
| }); | ||
| } | ||
| function limitBodyStream(stream, maxRequestBodySize) { | ||
| const reader = stream.getReader(); | ||
| let size = 0; | ||
| return new ReadableStream({ | ||
| async pull(controller) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| controller.close(); | ||
| return; | ||
| } | ||
| size += value.byteLength; | ||
| if (size > maxRequestBodySize) { | ||
| const error = createBodyTooLargeError(maxRequestBodySize); | ||
| reader.cancel(error).catch(() => {}); | ||
| controller.error(error); | ||
| return; | ||
| } | ||
| controller.enqueue(value); | ||
| }, | ||
| cancel(reason) { | ||
| return reader.cancel(reason); | ||
| } | ||
| }); | ||
| } | ||
| function limitRequestBody(request, maxRequestBodySize) { | ||
| if (!request.body) return request; | ||
| return new Request(request, { | ||
| body: limitBodyStream(request.body, maxRequestBodySize), | ||
| duplex: "half" | ||
| }); | ||
| } | ||
| export { createBodyTooLargeError, limitBodyStream, limitRequestBody }; |
@@ -29,2 +29,3 @@ function lazyInherit(target, source, sourceKey) { | ||
| const _needsNormRE = /(?:(?:^|\/)(?:\.|\.\.|%2e|%2e\.|\.%2e|%2e%2e)(?:\/|$))|[\\^#"<>{}`\x80-\uffff]/i; | ||
| const _searchNeedsNormRE = /[#"'<>]/; | ||
| const FastURL = /* @__PURE__ */ (() => { | ||
@@ -44,5 +45,5 @@ const NativeURL = globalThis.URL; | ||
| const isOriginForm = url[0] === "/"; | ||
| if (isOriginForm && !url.includes("#")) this.#href = url; | ||
| if (isOriginForm && !_searchNeedsNormRE.test(url)) this.#href = url; | ||
| else this.#url = new NativeURL(isOriginForm ? `http://localhost${url}` : url); | ||
| } else if (_needsNormRE.test(url.pathname) || url.search?.includes("#")) this.#url = new NativeURL(`${url.protocol || "http:"}//${url.host || "localhost"}${url.pathname}${url.search || ""}`); | ||
| } else if (_needsNormRE.test(url.pathname) || url.search && _searchNeedsNormRE.test(url.search)) this.#url = new NativeURL(`${url.protocol || "http:"}//${url.host || "localhost"}${url.pathname}${url.search || ""}`); | ||
| else { | ||
@@ -49,0 +50,0 @@ this.#protocol = url.protocol; |
@@ -56,2 +56,3 @@ import { FastURL } from "../_chunks/_url.mjs"; | ||
| error: this.options.error, | ||
| ...this.options.maxRequestBodySize !== void 0 ? { maxRequestBodySize: this.options.maxRequestBodySize } : {}, | ||
| ...this.options.bun, | ||
@@ -58,0 +59,0 @@ tls: { |
| import { FastURL } from "../_chunks/_url.mjs"; | ||
| import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions, toNativeResponse } from "../_chunks/_utils2.mjs"; | ||
| import { gracefulShutdownPlugin, wrapFetch } from "../_chunks/_plugins.mjs"; | ||
| import { limitRequestBody } from "../_chunks/_body-limit.mjs"; | ||
| const FastResponse = Response; | ||
@@ -34,3 +35,5 @@ function serve(options) { | ||
| this.waitUntil = this.#wait.waitUntil; | ||
| const maxRequestBodySize = this.options.maxRequestBodySize; | ||
| this.fetch = (request, info) => { | ||
| if (maxRequestBodySize !== void 0) request = limitRequestBody(request, maxRequestBodySize); | ||
| Object.defineProperties(request, { | ||
@@ -37,0 +40,0 @@ waitUntil: { value: this.#wait?.waitUntil }, |
@@ -7,2 +7,7 @@ import { FetchHandler, NodeHttpHandler, NodeServerRequest, NodeServerResponse, Server, ServerOptions, ServerRequest } from "../types.mjs"; | ||
| res?: NodeServerResponse; | ||
| /** | ||
| * Maximum allowed size (in bytes) for the request body, enforced for both the | ||
| * buffered reads and the streamed body. See `ServerOptions.maxRequestBodySize`. | ||
| */ | ||
| maxRequestBodySize?: number; | ||
| }; | ||
@@ -58,3 +63,5 @@ declare const NodeRequest: { | ||
| */ | ||
| declare function toNodeHandler(handler: FetchHandler & AdapterMeta): NodeHttpHandler & AdapterMeta; | ||
| declare function toNodeHandler(handler: FetchHandler & AdapterMeta, options?: { | ||
| maxRequestBodySize?: number; | ||
| }): NodeHttpHandler & AdapterMeta; | ||
| /** | ||
@@ -61,0 +68,0 @@ * Converts a Node.js HTTP handler into a Fetch API handler. |
+40
-20
| import { FastURL, lazyInherit } from "../_chunks/_url.mjs"; | ||
| import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions } from "../_chunks/_utils2.mjs"; | ||
| import { errorPlugin, gracefulShutdownPlugin, wrapFetch } from "../_chunks/_plugins.mjs"; | ||
| import { createBodyTooLargeError, limitBodyStream } from "../_chunks/_body-limit.mjs"; | ||
| import nodeHTTP, { IncomingMessage, ServerResponse } from "node:http"; | ||
@@ -161,14 +162,9 @@ import { Duplex, PassThrough, Readable, addAbortSignal } from "node:stream"; | ||
| get(name) { | ||
| if (this.#headers) return this.#headers.get(name); | ||
| const value = this.#req.headers[name.toLowerCase()]; | ||
| return Array.isArray(value) ? value.join(", ") : value || null; | ||
| return this._headers.get(name); | ||
| } | ||
| has(name) { | ||
| if (this.#headers) return this.#headers.has(name); | ||
| return name.toLowerCase() in this.#req.headers; | ||
| return this._headers.has(name); | ||
| } | ||
| getSetCookie() { | ||
| if (this.#headers) return this.#headers.getSetCookie(); | ||
| const value = this.#req.headers["set-cookie"]; | ||
| return Array.isArray(value) ? value : value ? [value] : []; | ||
| return this._headers.getSetCookie(); | ||
| } | ||
@@ -198,4 +194,6 @@ entries() { | ||
| #abortController; | ||
| #maxRequestBodySize; | ||
| constructor(ctx) { | ||
| this.#req = ctx.req; | ||
| this.#maxRequestBodySize = ctx.maxRequestBodySize; | ||
| this.runtime = { | ||
@@ -254,4 +252,5 @@ name: "node", | ||
| const method = this.method; | ||
| const hasBody = !(method === "GET" || method === "HEAD"); | ||
| this.#bodyStream = hasBody ? Readable.toWeb(this.#req) : null; | ||
| let stream = !(method === "GET" || method === "HEAD") ? Readable.toWeb(this.#req) : null; | ||
| if (stream && this.#maxRequestBodySize !== void 0) stream = limitBodyStream(stream, this.#maxRequestBodySize); | ||
| this.#bodyStream = stream; | ||
| } | ||
@@ -263,3 +262,3 @@ return this.#bodyStream; | ||
| if (this.#bodyStream !== void 0) return this.#bodyStream ? new Response(this.#bodyStream).text() : Promise.resolve(""); | ||
| return readBody(this.#req).then((buf) => buf.toString()); | ||
| return readBody(this.#req, this.#maxRequestBodySize).then((buf) => buf.toString()); | ||
| } | ||
@@ -306,15 +305,33 @@ json() { | ||
| } | ||
| function readBody(req) { | ||
| if ("rawBody" in req && Buffer.isBuffer(req.rawBody)) return Promise.resolve(req.rawBody); | ||
| function readBody(req, maxRequestBodySize) { | ||
| if ("rawBody" in req && Buffer.isBuffer(req.rawBody)) { | ||
| if (maxRequestBodySize !== void 0 && req.rawBody.length > maxRequestBodySize) return Promise.reject(createBodyTooLargeError(maxRequestBodySize)); | ||
| return Promise.resolve(req.rawBody); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| const chunks = []; | ||
| let size = 0; | ||
| const cleanup = () => { | ||
| req.off("data", onData); | ||
| req.off("end", onEnd); | ||
| req.off("error", onError); | ||
| }; | ||
| const onData = (chunk) => { | ||
| if (maxRequestBodySize !== void 0) { | ||
| size += chunk.length; | ||
| if (size > maxRequestBodySize) { | ||
| cleanup(); | ||
| req.pause?.(); | ||
| reject(createBodyTooLargeError(maxRequestBodySize)); | ||
| return; | ||
| } | ||
| } | ||
| chunks.push(chunk); | ||
| }; | ||
| const onError = (err) => { | ||
| cleanup(); | ||
| reject(err); | ||
| }; | ||
| const onEnd = () => { | ||
| req.off("error", onError); | ||
| req.off("data", onData); | ||
| cleanup(); | ||
| resolve(Buffer.concat(chunks)); | ||
@@ -416,4 +433,5 @@ }; | ||
| else headers.push([key, value]); | ||
| if (key === "content-type") hasContentTypeHeader = true; | ||
| else if (key === "content-length") hasContentLength = true; | ||
| const lowerKey = typeof key === "string" ? key.toLowerCase() : key; | ||
| if (lowerKey === "content-type") hasContentTypeHeader = true; | ||
| else if (lowerKey === "content-length") hasContentLength = true; | ||
| } | ||
@@ -733,3 +751,3 @@ if (contentType && !hasContentTypeHeader) headers.push(["content-type", contentType]); | ||
| } | ||
| function toNodeHandler(handler) { | ||
| function toNodeHandler(handler, options) { | ||
| if (handler.__nodeHandler) return handler.__nodeHandler; | ||
@@ -739,3 +757,4 @@ function convertedNodeHandler(nodeReq, nodeRes) { | ||
| req: nodeReq, | ||
| res: nodeRes | ||
| res: nodeRes, | ||
| maxRequestBodySize: options?.maxRequestBodySize | ||
| })); | ||
@@ -793,3 +812,4 @@ return res instanceof Promise ? res.then((resolvedRes) => sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); | ||
| req: nodeReq, | ||
| res: nodeRes | ||
| res: nodeRes, | ||
| maxRequestBodySize: this.options.maxRequestBodySize | ||
| }); | ||
@@ -796,0 +816,0 @@ request.waitUntil = this.#wait?.waitUntil; |
+1
-1
@@ -182,3 +182,3 @@ import { bold, cyan, gray, green, magenta, red, url, yellow } from "./_chunks/_utils.mjs"; | ||
| name: "srvx", | ||
| version: "0.11.17", | ||
| version: "0.11.18", | ||
| description: "Universal Server." | ||
@@ -185,0 +185,0 @@ }; |
+20
-0
@@ -101,2 +101,22 @@ import * as NodeHttp from "node:http"; | ||
| /** | ||
| * Maximum allowed size (in bytes) for the request body. | ||
| * | ||
| * As the body is read, its accumulated length is tracked and, once it exceeds | ||
| * this limit, reading is aborted and rejects with a `413`-style error (the error | ||
| * has `statusCode: 413`, `status: 413` and `code: "ERR_BODY_TOO_LARGE"`) so a | ||
| * handler can map it to an HTTP 413 (Payload Too Large) response. | ||
| * | ||
| * The limit covers the buffered reads (`request.text()` / `request.json()`) as | ||
| * well as the streamed body (`request.body`, and therefore `arrayBuffer()` / | ||
| * `blob()` / `bytes()` / `formData()`). | ||
| * | ||
| * Runtime support: | ||
| * - **Node**: enforced by srvx (body stream is size-limited). | ||
| * - **Bun**: mapped to Bun's native `maxRequestBodySize` (413 before the handler). | ||
| * - **Deno**: enforced by srvx (request body stream is size-limited). | ||
| * | ||
| * @default undefined (no limit) | ||
| */ | ||
| maxRequestBodySize?: number; | ||
| /** | ||
| * TLS server options. | ||
@@ -103,0 +123,0 @@ */ |
+11
-11
| { | ||
| "name": "srvx", | ||
| "version": "0.11.18", | ||
| "version": "0.11.19", | ||
| "description": "Universal Server.", | ||
@@ -62,4 +62,4 @@ "homepage": "https://srvx.h3.dev", | ||
| "devDependencies": { | ||
| "@cloudflare/workers-types": "^4.20260617.1", | ||
| "@hono/node-server": "^2.0.5", | ||
| "@cloudflare/workers-types": "^4.20260701.1", | ||
| "@hono/node-server": "^2.0.6", | ||
| "@mitata/counters": "^0.0.8", | ||
@@ -71,5 +71,5 @@ "@mjackson/node-fetch-server": "^0.7.0", | ||
| "@types/express": "^5.0.6", | ||
| "@types/node": "^25.9.3", | ||
| "@types/node": "^26.1.0", | ||
| "@types/node-forge": "^1.3.14", | ||
| "@types/serviceworker": "^0.0.197", | ||
| "@types/serviceworker": "^0.0.199", | ||
| "@typescript/native-preview": "latest", | ||
@@ -83,3 +83,3 @@ "@vitest/coverage-v8": "^4.1.9", | ||
| "express": "^5.2.1", | ||
| "fastify": "^5.8.5", | ||
| "fastify": "^5.9.0", | ||
| "get-port-please": "^3.2.0", | ||
@@ -89,9 +89,9 @@ "mdbox": "^0.1.1", | ||
| "node-forge": "^1.4.0", | ||
| "obuild": "^0.4.36", | ||
| "oxfmt": "^0.55.0", | ||
| "oxlint": "^1.70.0", | ||
| "srvx-release": "npm:srvx@^0.11.16", | ||
| "obuild": "^0.4.37", | ||
| "oxfmt": "^0.57.0", | ||
| "oxlint": "^1.72.0", | ||
| "srvx-release": "npm:srvx@^0.11.18", | ||
| "tslib": "^2.8.1", | ||
| "typescript": "^6.0.3", | ||
| "undici": "~8.3", | ||
| "undici": "~8.3.0", | ||
| "vitest": "^4.1.9" | ||
@@ -98,0 +98,0 @@ }, |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
117584
2.92%38
2.7%2597
2.53%