@mionjs/platform-uws 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,8 @@ import { DEFAULT_MAX_BODY_SIZE } from "@mionjs/core";
2
2
  //#region src/constants.ts
3
3
  var DEFAULT_UWS_HTTP_OPTIONS = {
4
4
  port: 80,
5
+ /** uWS' own default is 4 KB, which a 4 KB `?data=` query URL alone fills: same 8 KB as the node adapter */
6
+ maxHeaderSize: 8192,
5
7
  /** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */
6
8
  maxBodySize: DEFAULT_MAX_BODY_SIZE
7
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","names":[],"sources":["../../../src/constants.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {DEFAULT_MAX_BODY_SIZE} from '@mionjs/core';\nimport {UwsHttpOptions} from './types.ts';\n\nexport const DEFAULT_UWS_HTTP_OPTIONS: UwsHttpOptions = {\n port: 80,\n /** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */\n maxBodySize: DEFAULT_MAX_BODY_SIZE,\n};\n"],"mappings":";;AAUA,IAAa,2BAA2C;CACtD,MAAM;;CAEN,aAAa;AACf"}
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../../../src/constants.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {DEFAULT_MAX_BODY_SIZE} from '@mionjs/core';\nimport {UwsHttpOptions} from './types.ts';\n\nexport const DEFAULT_UWS_HTTP_OPTIONS: UwsHttpOptions = {\n port: 80,\n /** uWS' own default is 4 KB, which a 4 KB `?data=` query URL alone fills: same 8 KB as the node adapter */\n maxHeaderSize: 8192,\n /** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */\n maxBodySize: DEFAULT_MAX_BODY_SIZE,\n};\n"],"mappings":";;AAUA,IAAa,2BAA2C;CACtD,MAAM;;CAEN,eAAe;;CAEf,aAAa;AACf"}
@@ -2,6 +2,7 @@ import { AppOptions } from '@mionjs/bin-uws';
2
2
  export interface UwsHttpOptions {
3
3
  port: number;
4
4
  ssl?: AppOptions;
5
+ maxHeaderSize: number;
5
6
  maxBodySize: number;
6
7
  maxBodySizeCap?: number;
7
8
  }
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_UWS_HTTP_OPTIONS } from "./constants.js";
2
2
  import { headersFromUwsRequest } from "./headers.js";
3
- import { createContextFromChain, decodeQueryBody, dispatchMaybeSync, dispatchPlatformError, getDefaultHeaders, getRouterFatalErrorResponse, hostOwnsSocket, releaseHeaders, requestPayloadTooLarge, resetRouter, resolveExecutionChain, setPlatformConfig, toRpcError } from "@mionjs/router";
3
+ import { createContextFromChain, dispatchMaybeSync, dispatchPlatformError, getDefaultHeaders, getRouterFatalErrorResponse, hostOwnsSocket, releaseHeaders, requestPayloadTooLarge, resetRouter, resolveExecutionChain, setPlatformConfig, toRpcError } from "@mionjs/router";
4
4
  import { STATUS_CODES } from "http";
5
5
  import { loadUws } from "@mionjs/bin-uws";
6
6
  import { FatalError, SerializerModes, getENV } from "@mionjs/core";
@@ -32,6 +32,7 @@ async function startUwsServer(options) {
32
32
  port: httpOptions.port,
33
33
  httpOptions
34
34
  });
35
+ process.env.UWS_HTTP_MAX_HEADERS_SIZE ??= String(httpOptions.maxHeaderSize);
35
36
  const uws = loadUws();
36
37
  const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();
37
38
  app.any("/*", uwsRequestHandler);
@@ -64,18 +65,35 @@ async function startUwsServer(options) {
64
65
  });
65
66
  });
66
67
  }
68
+ var LOWER_CASE_METHODS = Object.assign(Object.create(null), {
69
+ get: "GET",
70
+ head: "HEAD",
71
+ post: "POST",
72
+ put: "PUT",
73
+ delete: "DELETE",
74
+ connect: "CONNECT",
75
+ options: "OPTIONS",
76
+ trace: "TRACE",
77
+ patch: "PATCH",
78
+ query: "QUERY"
79
+ });
80
+ function upperCaseMethod(method) {
81
+ return LOWER_CASE_METHODS[method] ?? "OTHER";
82
+ }
67
83
  function drainRequestBody(res) {
68
84
  res.onData(() => {});
69
85
  }
70
86
  function uwsRequestHandler(res, req) {
71
- const state = {
72
- replied: false,
73
- aborted: false
74
- };
75
87
  const path = req.getUrl();
76
88
  const query = req.getQuery();
77
89
  const urlQuery = query === "" ? void 0 : query;
90
+ const method = upperCaseMethod(req.getMethod());
78
91
  const reqHeaders = headersFromUwsRequest(req);
92
+ const state = {
93
+ replied: false,
94
+ aborted: false,
95
+ head: method === "HEAD"
96
+ };
79
97
  const respHeaders = getDefaultHeaders();
80
98
  res.onAborted(() => {
81
99
  state.aborted = true;
@@ -94,21 +112,9 @@ function uwsRequestHandler(res, req) {
94
112
  fatalFail(res, state, respHeaders, toRpcError(err));
95
113
  return;
96
114
  }
97
- const dispatchBody = (rawBody, readQueryBody) => {
98
- let reqRawBody = rawBody;
99
- let reqBodyType = SerializerModes.stringifyJson;
100
- try {
101
- const queryBody = readQueryBody ? decodeQueryBody(urlQuery, reqRawBody || void 0) : void 0;
102
- if (queryBody) {
103
- reqRawBody = queryBody.rawBody;
104
- reqBodyType = queryBody.bodyType;
105
- }
106
- } catch (err) {
107
- state.replied = true;
108
- fatalFail(res, state, respHeaders, err);
109
- return;
110
- }
111
- const context = createContextFromChain(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);
115
+ const dispatchBody = (rawBody) => {
116
+ const reqBodyType = SerializerModes.stringifyJson;
117
+ const context = createContextFromChain(chain, path, urlQuery, method, reqHeaders, respHeaders, rawBody, reqBodyType);
112
118
  let dispatched;
113
119
  try {
114
120
  dispatched = dispatchMaybeSync(context, rawRequest, res);
@@ -136,17 +142,17 @@ function uwsRequestHandler(res, req) {
136
142
  };
137
143
  if (!chain.readsBody) {
138
144
  drainRequestBody(res);
139
- dispatchBody("", false);
145
+ dispatchBody("");
140
146
  return;
141
147
  }
142
148
  res.collectBody(chain.maxBodySize, (fullBody) => {
143
149
  if (state.replied) return;
144
150
  if (fullBody === null) {
145
- answerWith(dispatchPlatformError(chain, path, urlQuery, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res));
151
+ answerWith(dispatchPlatformError(chain, path, urlQuery, method, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res));
146
152
  return;
147
153
  }
148
154
  if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {
149
- dispatchBody(fullBody.byteLength === 0 ? "" : Buffer.from(fullBody).toString(), true);
155
+ dispatchBody(fullBody.byteLength === 0 ? "" : Buffer.from(fullBody).toString());
150
156
  return;
151
157
  }
152
158
  queueMicrotask(() => {
@@ -160,7 +166,7 @@ function uwsRequestHandler(res, req) {
160
166
  }));
161
167
  return;
162
168
  }
163
- dispatchBody(releaseAfterDecode(fullBody), true);
169
+ dispatchBody(releaseAfterDecode(fullBody));
164
170
  });
165
171
  });
166
172
  }
@@ -177,13 +183,6 @@ function releaseAfterDecode(fullBody) {
177
183
  function fatalFail(res, state, respHeaders, error) {
178
184
  reply(res, state, getRouterFatalErrorResponse(error, respHeaders));
179
185
  }
180
- function isHeaderSafe(text) {
181
- for (let i = 0; i < text.length; i++) {
182
- const code = text.charCodeAt(i);
183
- if (code === 13 || code === 10 || code === 0) return false;
184
- }
185
- return true;
186
- }
187
186
  /** The line is a pure function of the code, so it is built once per code rather than per response. */
188
187
  var statusLines = /* @__PURE__ */ new Map();
189
188
  function statusLine(statusCode) {
@@ -198,19 +197,19 @@ function reply(res, state, mionResp) {
198
197
  if (state.aborted) return;
199
198
  const headers = mionResp.headers;
200
199
  const payload = JSON.stringify(mionResp.body);
201
- const write = (name, value) => {
202
- if (isHeaderSafe(value)) res.writeHeader(name, value);
203
- };
200
+ const close = headers.get("connection")?.toLowerCase() === "close";
201
+ const write = (name, value) => res.writeHeader(name, value);
204
202
  res.cork(() => {
205
203
  res.writeStatus(statusLine(mionResp.statusCode));
206
204
  headers.forEach((name, value) => {
207
- if (name === "content-length" || name === "content-type" || !isHeaderSafe(name)) return;
205
+ if (name === "content-length" || name === "content-type") return;
208
206
  if (typeof value === "string") write(name, value);
209
207
  else if (name === "set-cookie") for (const cookie of value) write(name, cookie);
210
208
  else write(name, value.join(", "));
211
209
  });
212
210
  res.writeHeader("content-type", mionResp.contentType);
213
- res.end(payload);
211
+ if (state.head) res.endWithoutBody(Buffer.byteLength(payload, "utf8"), close);
212
+ else res.end(payload, close);
214
213
  });
215
214
  releaseHeaders(headers);
216
215
  }
@@ -1 +1 @@
1
- {"version":3,"file":"uwsHttp.js","names":[],"sources":["../../../src/uwsHttp.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {\n dispatchMaybeSync,\n dispatchPlatformError,\n resolveExecutionChain,\n createContextFromChain,\n getRouterFatalErrorResponse,\n toRpcError,\n resetRouter,\n decodeQueryBody,\n setPlatformConfig,\n getDefaultHeaders,\n releaseHeaders,\n requestPayloadTooLarge,\n hostOwnsSocket,\n} from '@mionjs/router';\nimport {STATUS_CODES} from 'http';\nimport {loadUws} from '@mionjs/bin-uws';\nimport type {HttpRequest, HttpResponse, TemplatedApp, us_listen_socket} from '@mionjs/bin-uws';\nimport {DEFAULT_UWS_HTTP_OPTIONS} from './constants.ts';\nimport type {UwsHttpOptions} from './types.ts';\nimport type {MethodsExecutionChain, MionHeaders, MionResponse, RecordResponseHeaders} from '@mionjs/router';\nimport {getENV, SerializerModes} from '@mionjs/core';\nimport type {SerializerCode} from '@mionjs/core';\nimport {RpcError, FatalError} from '@mionjs/core';\nimport {headersFromUwsRequest} from './headers.ts';\n\n// ############# PRIVATE STATE #############\n\nlet httpOptions: Readonly<UwsHttpOptions> = {...DEFAULT_UWS_HTTP_OPTIONS};\n\n// The most bytes one uWS socket read can deliver: uSockets' LIBUS_RECV_BUFFER_LENGTH (512 KiB) at the tag pinned in\n// packages/bin-uws. A LARGER body cannot be single-read, which is what makes the zero-copy branch below safe (a\n// detachment tripwire guards it at runtime). Re-verify against uSockets on a tag bump.\nconst UWS_MAX_SINGLE_READ = 524288;\n\n/** The running server: the uWS app plus the socket handle listen() produced. */\nexport interface UwsServer {\n app: TemplatedApp;\n listenSocket: us_listen_socket;\n close(): void;\n}\n\n// ############# PUBLIC METHODS #############\n\nexport function resetUwsHttpOpts() {\n httpOptions = {...DEFAULT_UWS_HTTP_OPTIONS};\n resetRouter();\n}\n\nexport function setUwsHttpOpts(options?: Partial<UwsHttpOptions>) {\n httpOptions = {\n ...httpOptions,\n ...options,\n };\n\n return httpOptions;\n}\n\n/** The platform config the router publishes: everything but the TLS file paths. */\nfunction serializablePlatformConfig(): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {ssl: _ssl, ...serializableConfig} = httpOptions;\n return serializableConfig;\n}\n\nexport async function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer> {\n const isTest = getENV('NODE_ENV') === 'test';\n\n if (options) setUwsHttpOpts(options);\n // uWS is its own C++ event loop and owns its listen socket, so it cannot mount on a host node server.\n if (hostOwnsSocket()) {\n throw new Error(\n '@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen ' +\n 'socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.'\n );\n }\n const protocol = httpOptions.ssl ? 'https' : 'http';\n const port = httpOptions.port !== 80 ? `:${httpOptions.port}` : '';\n const url = `${protocol}://localhost${port}`;\n if (!isTest)\n console.log(`mion uws server running on ${url}`, {\n port: httpOptions.port,\n httpOptions,\n });\n\n const uws = loadUws();\n const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();\n app.any('/*', uwsRequestHandler);\n\n return new Promise<UwsServer>((resolve, reject) => {\n app.listen(httpOptions.port, (listenSocket) => {\n if (!listenSocket) {\n reject(new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));\n return;\n }\n\n setPlatformConfig(serializablePlatformConfig());\n\n let closed = false;\n const close = () => {\n if (closed) return;\n closed = true;\n uws.us_listen_socket_close(listenSocket);\n };\n const server: UwsServer = {app, listenSocket, close};\n\n const shutdownHandler = function () {\n if (!isTest) console.log(`Shutting down mion server on ${url}`);\n close();\n process.exit(0);\n };\n process.on('SIGINT', shutdownHandler);\n process.on('SIGTERM', shutdownHandler);\n\n resolve(server);\n });\n });\n}\n\n// ############# PRIVATE METHODS #############\n\n// uWS refreshes a socket's idle timeout only from inside a body-data callback, and runs it only when a reader is registered.\n// Without one, a request answered before its body finished arriving is closed mid-upload, so drop every chunk instead.\nfunction drainRequestBody(res: HttpResponse) {\n res.onData(() => {});\n}\n\n// exported for tests and for mounting on a hand-built uWS app; NOT a middleware handler (see startUwsServer).\n// uWS contract: `req` is valid only synchronously here, so the async dispatch snapshots it before the first await.\nexport function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void {\n const state = {replied: false, aborted: false};\n // Everything read from `req` happens HERE, synchronously.\n const path = req.getUrl();\n const query = req.getQuery();\n const urlQuery = query === '' ? undefined : query;\n const reqHeaders = headersFromUwsRequest(req);\n\n const respHeaders = getDefaultHeaders();\n\n // must be registered before any async work: after a disconnect uWS frees the response, and touching it would crash\n res.onAborted(() => {\n state.aborted = true;\n });\n\n // route resolved BEFORE the body, synchronously: the native read stops at the chain's limit, and a late context stays GC-cheap.\n // The raw request object is built once: the one a pathTransform reads and the one the handlers see.\n const rawRequest = {path, urlQuery, headers: reqHeaders};\n let chain: MethodsExecutionChain;\n try {\n chain = resolveExecutionChain(path, urlQuery, rawRequest);\n } catch (err) {\n drainRequestBody(res);\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n return;\n }\n\n const dispatchBody = (rawBody: string, readQueryBody: boolean) => {\n let reqRawBody: any = rawBody;\n let reqBodyType: SerializerCode = SerializerModes.stringifyJson;\n // a throw here runs inside uWS' native callback (or a microtask): it must become a response\n try {\n const queryBody = readQueryBody ? decodeQueryBody(urlQuery, reqRawBody || undefined) : undefined;\n if (queryBody) {\n reqRawBody = queryBody.rawBody;\n reqBodyType = queryBody.bodyType;\n }\n } catch (err) {\n state.replied = true;\n fatalFail(res, state, respHeaders, err as RpcError<string>);\n return;\n }\n\n const context = createContextFromChain(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);\n let dispatched: MionResponse | Promise<MionResponse>;\n try {\n dispatched = dispatchMaybeSync(context, rawRequest, res);\n } catch (err) {\n failWith(err);\n return;\n }\n // an all-sync chain answers inside uWS' own callback, with no promise in between\n if (dispatched instanceof Promise) answerWith(dispatched);\n else answer(dispatched);\n };\n\n const answer = (mionResponse: MionResponse) => {\n if (state.replied) return;\n state.replied = true;\n try {\n reply(res, state, mionResponse);\n } catch {\n // already marked replied: a failed write has nothing left to answer with\n }\n };\n\n const failWith = (err: unknown) => {\n if (state.replied) return;\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n };\n\n const answerWith = (dispatched: Promise<MionResponse>) => {\n dispatched.then(answer).catch(failWith);\n };\n\n // collectBody assembles the body natively (uWS' onDataV2 preallocates) and calls back ONCE, with null past maxSize,\n // which is exactly the maxBodySize contract. A not-found chain has no route to feed: its body is dropped as it arrives.\n if (!chain.readsBody) {\n drainRequestBody(res);\n dispatchBody('', false);\n return;\n }\n\n res.collectBody(chain.maxBodySize, (fullBody) => {\n if (state.replied) return;\n if (fullBody === null) {\n // the route resolved, so the refusal still runs the chain's alwaysRun members\n answerWith(\n dispatchPlatformError(chain, path, urlQuery, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res)\n );\n return;\n }\n\n // collectBody has two paths (verified in the pinned tag's HttpResponseWrapper.h and by test): a single-read body is a\n // zero-copy window into uWS' receive buffer, DETACHED when this callback returns, so it must be decoded synchronously\n // here; a multi-read body was assembled in C++ and OWNERSHIP-TRANSFERRED to JS, so it can be decoded from a microtask.\n if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {\n // a body-less request (every GET) skips the view and the decode altogether\n dispatchBody(fullBody.byteLength === 0 ? '' : Buffer.from(fullBody).toString(), true);\n return;\n }\n // bigger than one read can deliver → guaranteed the ownership-transferred path, so the buffer survives the microtask.\n // The zero-length check is a tripwire for an upstream change: fail loudly instead of parsing a neutered buffer.\n queueMicrotask(() => {\n if (state.replied) return;\n if (fullBody.byteLength === 0) {\n state.replied = true;\n const error = new FatalError({\n publicMessage: 'Internal Server Error',\n type: 'unknown-error',\n errorData: {reason: 'uws detached a multi-read body buffer (upstream behavior change) — report to mion'},\n });\n fatalFail(res, state, respHeaders, error);\n return;\n }\n dispatchBody(releaseAfterDecode(fullBody), true);\n });\n });\n}\n\n/** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.\n * `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.\n * Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */\nfunction releaseAfterDecode(fullBody: ArrayBuffer): string {\n const text = Buffer.from(fullBody).toString();\n try {\n (fullBody as ReleasableBuffer).transfer?.(0);\n } catch {\n // not transferable: the collector gets it instead, exactly as before\n }\n return text;\n}\n\n/** `transfer` is ES2024 and this package compiles against an ES2023 lib; optional because a runtime without it must work. */\ntype ReleasableBuffer = ArrayBuffer & {transfer?: (newByteLength?: number) => ArrayBuffer};\n\n// only called when there is an http error or weird unhandled route errors\nfunction fatalFail(res: HttpResponse, state: {aborted: boolean}, respHeaders: MionHeaders, error: RpcError<string>) {\n const routeResponse = getRouterFatalErrorResponse(error, respHeaders);\n reply(res, state, routeResponse);\n}\n\nfunction isHeaderSafe(text: string): boolean {\n // one pass, no allocation: a CR, LF or NUL anywhere makes the header unsafe to write\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (code === 13 || code === 10 || code === 0) return false;\n }\n return true;\n}\n\n/** The line is a pure function of the code, so it is built once per code rather than per response. */\nconst statusLines = new Map<number, string>();\nfunction statusLine(statusCode: number): string {\n const cached = statusLines.get(statusCode);\n if (cached !== undefined) return cached;\n const statusText = STATUS_CODES[statusCode];\n const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;\n statusLines.set(statusCode, line);\n return line;\n}\n\nfunction reply(res: HttpResponse, state: {aborted: boolean}, mionResp: MionResponse) {\n // uWS freed the response when the client left; touching it would crash\n if (state.aborted) return;\n const headers = mionResp.headers as RecordResponseHeaders;\n\n // serialized BEFORE the cork: uWS warns a cork buffer must not be held across event loop iterations\n const payload = JSON.stringify(mionResp.body);\n\n // cork makes one syscall; uWS headers are write-only and must all precede end().\n // content-length is skipped: uWS writes its own, and a duplicate corrupts the response.\n // uWS writes headers unchecked, so an echoed CR or LF would be header injection: dropped\n const write = (name: string, value: string) => {\n if (isHeaderSafe(value)) res.writeHeader(name, value);\n };\n res.cork(() => {\n res.writeStatus(statusLine(mionResp.statusCode));\n headers.forEach((name, value) => {\n if (name === 'content-length' || name === 'content-type' || !isHeaderSafe(name)) return;\n if (typeof value === 'string') write(name, value);\n // RFC 6265 forbids folding set-cookie; uWS writes a repeated name as its own line\n else if (name === 'set-cookie') for (const cookie of value) write(name, cookie);\n else write(name, value.join(', '));\n });\n // last, and the store's own skipped above: uWS appends a repeated name instead of replacing it\n res.writeHeader('content-type', mionResp.contentType);\n\n res.end(payload);\n });\n releaseHeaders(headers);\n}\n"],"mappings":";;;;;;;AAmCA,IAAI,cAAwC,EAAC,GAAG,yBAAwB;AAKxE,IAAM,sBAAsB;AAW5B,SAAgB,mBAAmB;CACjC,cAAc,EAAC,GAAG,yBAAwB;CAC1C,YAAY;AACd;AAEA,SAAgB,eAAe,SAAmC;CAChE,cAAc;EACZ,GAAG;EACH,GAAG;CACL;CAEA,OAAO;AACT;;AAGA,SAAS,6BAAsD;CAE7D,MAAM,EAAC,KAAK,MAAM,GAAG,uBAAsB;CAC3C,OAAO;AACT;AAEA,eAAsB,eAAe,SAAuD;CAC1F,MAAM,SAAS,OAAO,UAAU,MAAM;CAEtC,IAAI,SAAS,eAAe,OAAO;CAEnC,IAAI,eAAe,GACjB,MAAM,IAAI,MACR,yLAEF;CAIF,MAAM,MAAM,GAFK,YAAY,MAAM,UAAU,OAErB,cADX,YAAY,SAAS,KAAK,IAAI,YAAY,SAAS;CAEhE,IAAI,CAAC,QACH,QAAQ,IAAI,8BAA8B,OAAO;EAC/C,MAAM,YAAY;EAClB;CACF,CAAC;CAEH,MAAM,MAAM,QAAQ;CACpB,MAAM,MAAM,YAAY,MAAM,IAAI,OAAO,YAAY,GAAG,IAAI,IAAI,IAAI;CACpE,IAAI,IAAI,MAAM,iBAAiB;CAE/B,OAAO,IAAI,SAAoB,SAAS,WAAW;EACjD,IAAI,OAAO,YAAY,OAAO,iBAAiB;GAC7C,IAAI,CAAC,cAAc;IACjB,uBAAO,IAAI,MAAM,4CAA4C,YAAY,KAAK,gBAAgB,CAAC;IAC/F;GACF;GAEA,kBAAkB,2BAA2B,CAAC;GAE9C,IAAI,SAAS;GACb,MAAM,cAAc;IAClB,IAAI,QAAQ;IACZ,SAAS;IACT,IAAI,uBAAuB,YAAY;GACzC;GACA,MAAM,SAAoB;IAAC;IAAK;IAAc;GAAK;GAEnD,MAAM,kBAAkB,WAAY;IAClC,IAAI,CAAC,QAAQ,QAAQ,IAAI,gCAAgC,KAAK;IAC9D,MAAM;IACN,QAAQ,KAAK,CAAC;GAChB;GACA,QAAQ,GAAG,UAAU,eAAe;GACpC,QAAQ,GAAG,WAAW,eAAe;GAErC,QAAQ,MAAM;EAChB,CAAC;CACH,CAAC;AACH;AAMA,SAAS,iBAAiB,KAAmB;CAC3C,IAAI,aAAa,CAAC,CAAC;AACrB;AAIA,SAAgB,kBAAkB,KAAmB,KAAwB;CAC3E,MAAM,QAAQ;EAAC,SAAS;EAAO,SAAS;CAAK;CAE7C,MAAM,OAAO,IAAI,OAAO;CACxB,MAAM,QAAQ,IAAI,SAAS;CAC3B,MAAM,WAAW,UAAU,KAAK,KAAA,IAAY;CAC5C,MAAM,aAAa,sBAAsB,GAAG;CAE5C,MAAM,cAAc,kBAAkB;CAGtC,IAAI,gBAAgB;EAClB,MAAM,UAAU;CAClB,CAAC;CAID,MAAM,aAAa;EAAC;EAAM;EAAU,SAAS;CAAU;CACvD,IAAI;CACJ,IAAI;EACF,QAAQ,sBAAsB,MAAM,UAAU,UAAU;CAC1D,SAAS,KAAK;EACZ,iBAAiB,GAAG;EACpB,MAAM,UAAU;EAChB,UAAU,KAAK,OAAO,aAAa,WAAW,GAAG,CAAC;EAClD;CACF;CAEA,MAAM,gBAAgB,SAAiB,kBAA2B;EAChE,IAAI,aAAkB;EACtB,IAAI,cAA8B,gBAAgB;EAElD,IAAI;GACF,MAAM,YAAY,gBAAgB,gBAAgB,UAAU,cAAc,KAAA,CAAS,IAAI,KAAA;GACvF,IAAI,WAAW;IACb,aAAa,UAAU;IACvB,cAAc,UAAU;GAC1B;EACF,SAAS,KAAK;GACZ,MAAM,UAAU;GAChB,UAAU,KAAK,OAAO,aAAa,GAAuB;GAC1D;EACF;EAEA,MAAM,UAAU,uBAAuB,OAAO,MAAM,UAAU,YAAY,aAAa,YAAY,WAAW;EAC9G,IAAI;EACJ,IAAI;GACF,aAAa,kBAAkB,SAAS,YAAY,GAAG;EACzD,SAAS,KAAK;GACZ,SAAS,GAAG;GACZ;EACF;EAEA,IAAI,sBAAsB,SAAS,WAAW,UAAU;OACnD,OAAO,UAAU;CACxB;CAEA,MAAM,UAAU,iBAA+B;EAC7C,IAAI,MAAM,SAAS;EACnB,MAAM,UAAU;EAChB,IAAI;GACF,MAAM,KAAK,OAAO,YAAY;EAChC,QAAQ,CAER;CACF;CAEA,MAAM,YAAY,QAAiB;EACjC,IAAI,MAAM,SAAS;EACnB,MAAM,UAAU;EAChB,UAAU,KAAK,OAAO,aAAa,WAAW,GAAG,CAAC;CACpD;CAEA,MAAM,cAAc,eAAsC;EACxD,WAAW,KAAK,MAAM,EAAE,MAAM,QAAQ;CACxC;CAIA,IAAI,CAAC,MAAM,WAAW;EACpB,iBAAiB,GAAG;EACpB,aAAa,IAAI,KAAK;EACtB;CACF;CAEA,IAAI,YAAY,MAAM,cAAc,aAAa;EAC/C,IAAI,MAAM,SAAS;EACnB,IAAI,aAAa,MAAM;GAErB,WACE,sBAAsB,OAAO,MAAM,UAAU,uBAAuB,GAAG,YAAY,aAAa,YAAY,GAAG,CACjH;GACA;EACF;EAKA,IAAI,SAAS,cAAc,qBAAqB;GAE9C,aAAa,SAAS,eAAe,IAAI,KAAK,OAAO,KAAK,QAAQ,EAAE,SAAS,GAAG,IAAI;GACpF;EACF;EAGA,qBAAqB;GACnB,IAAI,MAAM,SAAS;GACnB,IAAI,SAAS,eAAe,GAAG;IAC7B,MAAM,UAAU;IAMhB,UAAU,KAAK,OAAO,aAAa,IALjB,WAAW;KAC3B,eAAe;KACf,MAAM;KACN,WAAW,EAAC,QAAQ,oFAAmF;IACzG,CACmC,CAAK;IACxC;GACF;GACA,aAAa,mBAAmB,QAAQ,GAAG,IAAI;EACjD,CAAC;CACH,CAAC;AACH;;;;AAKA,SAAS,mBAAmB,UAA+B;CACzD,MAAM,OAAO,OAAO,KAAK,QAAQ,EAAE,SAAS;CAC5C,IAAI;EACF,SAA+B,WAAW,CAAC;CAC7C,QAAQ,CAER;CACA,OAAO;AACT;AAMA,SAAS,UAAU,KAAmB,OAA2B,aAA0B,OAAyB;CAElH,MAAM,KAAK,OADW,4BAA4B,OAAO,WACvC,CAAa;AACjC;AAEA,SAAS,aAAa,MAAuB;CAE3C,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,OAAO,KAAK,WAAW,CAAC;EAC9B,IAAI,SAAS,MAAM,SAAS,MAAM,SAAS,GAAG,OAAO;CACvD;CACA,OAAO;AACT;;AAGA,IAAM,8BAAc,IAAI,IAAoB;AAC5C,SAAS,WAAW,YAA4B;CAC9C,MAAM,SAAS,YAAY,IAAI,UAAU;CACzC,IAAI,WAAW,KAAA,GAAW,OAAO;CACjC,MAAM,aAAa,aAAa;CAChC,MAAM,OAAO,aAAa,GAAG,WAAW,GAAG,eAAe,GAAG;CAC7D,YAAY,IAAI,YAAY,IAAI;CAChC,OAAO;AACT;AAEA,SAAS,MAAM,KAAmB,OAA2B,UAAwB;CAEnF,IAAI,MAAM,SAAS;CACnB,MAAM,UAAU,SAAS;CAGzB,MAAM,UAAU,KAAK,UAAU,SAAS,IAAI;CAK5C,MAAM,SAAS,MAAc,UAAkB;EAC7C,IAAI,aAAa,KAAK,GAAG,IAAI,YAAY,MAAM,KAAK;CACtD;CACA,IAAI,WAAW;EACb,IAAI,YAAY,WAAW,SAAS,UAAU,CAAC;EAC/C,QAAQ,SAAS,MAAM,UAAU;GAC/B,IAAI,SAAS,oBAAoB,SAAS,kBAAkB,CAAC,aAAa,IAAI,GAAG;GACjF,IAAI,OAAO,UAAU,UAAU,MAAM,MAAM,KAAK;QAE3C,IAAI,SAAS,cAAc,KAAK,MAAM,UAAU,OAAO,MAAM,MAAM,MAAM;QACzE,MAAM,MAAM,MAAM,KAAK,IAAI,CAAC;EACnC,CAAC;EAED,IAAI,YAAY,gBAAgB,SAAS,WAAW;EAEpD,IAAI,IAAI,OAAO;CACjB,CAAC;CACD,eAAe,OAAO;AACxB"}
1
+ {"version":3,"file":"uwsHttp.js","names":[],"sources":["../../../src/uwsHttp.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {\n dispatchMaybeSync,\n dispatchPlatformError,\n resolveExecutionChain,\n createContextFromChain,\n getRouterFatalErrorResponse,\n toRpcError,\n resetRouter,\n setPlatformConfig,\n getDefaultHeaders,\n releaseHeaders,\n requestPayloadTooLarge,\n hostOwnsSocket,\n HttpMethod,\n} from '@mionjs/router';\nimport {STATUS_CODES} from 'http';\nimport {loadUws} from '@mionjs/bin-uws';\nimport type {HttpRequest, HttpResponse, TemplatedApp, us_listen_socket} from '@mionjs/bin-uws';\nimport {DEFAULT_UWS_HTTP_OPTIONS} from './constants.ts';\nimport type {UwsHttpOptions} from './types.ts';\nimport type {MethodsExecutionChain, MionHeaders, MionResponse, RecordResponseHeaders} from '@mionjs/router';\nimport {getENV, SerializerModes} from '@mionjs/core';\nimport type {SerializerCode} from '@mionjs/core';\nimport {RpcError, FatalError} from '@mionjs/core';\nimport {headersFromUwsRequest} from './headers.ts';\n\n// ############# PRIVATE STATE #############\n\nlet httpOptions: Readonly<UwsHttpOptions> = {...DEFAULT_UWS_HTTP_OPTIONS};\n\n// The most bytes one uWS socket read can deliver: uSockets' LIBUS_RECV_BUFFER_LENGTH (512 KiB) at the tag pinned in\n// packages/bin-uws. A LARGER body cannot be single-read, which is what makes the zero-copy branch below safe (a\n// detachment tripwire guards it at runtime). Re-verify against uSockets on a tag bump.\nconst UWS_MAX_SINGLE_READ = 524288;\n\n/** The running server: the uWS app plus the socket handle listen() produced. */\nexport interface UwsServer {\n app: TemplatedApp;\n listenSocket: us_listen_socket;\n close(): void;\n}\n\n// ############# PUBLIC METHODS #############\n\nexport function resetUwsHttpOpts() {\n httpOptions = {...DEFAULT_UWS_HTTP_OPTIONS};\n resetRouter();\n}\n\nexport function setUwsHttpOpts(options?: Partial<UwsHttpOptions>) {\n httpOptions = {\n ...httpOptions,\n ...options,\n };\n\n return httpOptions;\n}\n\n/** The platform config the router publishes: everything but the TLS file paths. */\nfunction serializablePlatformConfig(): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {ssl: _ssl, ...serializableConfig} = httpOptions;\n return serializableConfig;\n}\n\nexport async function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer> {\n const isTest = getENV('NODE_ENV') === 'test';\n\n if (options) setUwsHttpOpts(options);\n // uWS is its own C++ event loop and owns its listen socket, so it cannot mount on a host node server.\n if (hostOwnsSocket()) {\n throw new Error(\n '@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen ' +\n 'socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.'\n );\n }\n const protocol = httpOptions.ssl ? 'https' : 'http';\n const port = httpOptions.port !== 80 ? `:${httpOptions.port}` : '';\n const url = `${protocol}://localhost${port}`;\n if (!isTest)\n console.log(`mion uws server running on ${url}`, {\n port: httpOptions.port,\n httpOptions,\n });\n\n // uWS reads it once, when its binary loads\n process.env.UWS_HTTP_MAX_HEADERS_SIZE ??= String(httpOptions.maxHeaderSize);\n const uws = loadUws();\n const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();\n app.any('/*', uwsRequestHandler);\n\n return new Promise<UwsServer>((resolve, reject) => {\n app.listen(httpOptions.port, (listenSocket) => {\n if (!listenSocket) {\n reject(new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));\n return;\n }\n\n setPlatformConfig(serializablePlatformConfig());\n\n let closed = false;\n const close = () => {\n if (closed) return;\n closed = true;\n uws.us_listen_socket_close(listenSocket);\n };\n const server: UwsServer = {app, listenSocket, close};\n\n const shutdownHandler = function () {\n if (!isTest) console.log(`Shutting down mion server on ${url}`);\n close();\n process.exit(0);\n };\n process.on('SIGINT', shutdownHandler);\n process.on('SIGTERM', shutdownHandler);\n\n resolve(server);\n });\n });\n}\n\n// ############# PRIVATE METHODS #############\n\n// uWS hands the method over lowercased\n// null prototype: a `constructor` method off the wire must not read Object.prototype\nconst LOWER_CASE_METHODS: Readonly<Record<string, HttpMethod>> = Object.assign(Object.create(null), {\n get: 'GET',\n head: 'HEAD',\n post: 'POST',\n put: 'PUT',\n delete: 'DELETE',\n connect: 'CONNECT',\n options: 'OPTIONS',\n trace: 'TRACE',\n patch: 'PATCH',\n query: 'QUERY',\n});\nfunction upperCaseMethod(method: string): HttpMethod {\n return LOWER_CASE_METHODS[method] ?? 'OTHER';\n}\n\n// uWS refreshes a socket's idle timeout only from inside a body-data callback, and runs it only when a reader is registered.\n// Without one, a request answered before its body finished arriving is closed mid-upload, so drop every chunk instead.\nfunction drainRequestBody(res: HttpResponse) {\n res.onData(() => {});\n}\n\n// exported for tests and for mounting on a hand-built uWS app; NOT a middleware handler (see startUwsServer).\n// uWS contract: `req` is valid only synchronously here, so the async dispatch snapshots it before the first await.\nexport function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void {\n const path = req.getUrl();\n const query = req.getQuery();\n const urlQuery = query === '' ? undefined : query;\n const method = upperCaseMethod(req.getMethod());\n const reqHeaders = headersFromUwsRequest(req);\n const state: ReplyState = {replied: false, aborted: false, head: method === 'HEAD'};\n\n const respHeaders = getDefaultHeaders();\n\n // must be registered before any async work: after a disconnect uWS frees the response, and touching it would crash\n res.onAborted(() => {\n state.aborted = true;\n });\n\n // route resolved BEFORE the body: the native read stops at the chain's limit, and a late context stays GC-cheap.\n // The raw request object is built once: the one a pathTransform reads and the one the handlers see.\n const rawRequest = {path, urlQuery, headers: reqHeaders};\n let chain: MethodsExecutionChain;\n try {\n chain = resolveExecutionChain(path, urlQuery, rawRequest);\n } catch (err) {\n drainRequestBody(res);\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n return;\n }\n\n const dispatchBody = (rawBody: string) => {\n const reqBodyType: SerializerCode = SerializerModes.stringifyJson;\n\n const context = createContextFromChain(chain, path, urlQuery, method, reqHeaders, respHeaders, rawBody, reqBodyType);\n let dispatched: MionResponse | Promise<MionResponse>;\n try {\n dispatched = dispatchMaybeSync(context, rawRequest, res);\n } catch (err) {\n failWith(err);\n return;\n }\n // an all-sync chain answers inside uWS' own callback, with no promise in between\n if (dispatched instanceof Promise) answerWith(dispatched);\n else answer(dispatched);\n };\n\n const answer = (mionResponse: MionResponse) => {\n if (state.replied) return;\n state.replied = true;\n try {\n reply(res, state, mionResponse);\n } catch {\n // already marked replied: a failed write has nothing left to answer with\n }\n };\n\n const failWith = (err: unknown) => {\n if (state.replied) return;\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n };\n\n const answerWith = (dispatched: Promise<MionResponse>) => {\n dispatched.then(answer).catch(failWith);\n };\n\n // collectBody assembles the body natively (uWS' onDataV2 preallocates) and calls back ONCE, with null past maxSize,\n // which is exactly the maxBodySize contract. A not-found chain has no route to feed: its body is dropped as it arrives.\n if (!chain.readsBody) {\n drainRequestBody(res);\n dispatchBody('');\n return;\n }\n\n res.collectBody(chain.maxBodySize, (fullBody) => {\n if (state.replied) return;\n if (fullBody === null) {\n // the route resolved, so the refusal still runs the chain's alwaysRun members\n answerWith(\n dispatchPlatformError(chain, path, urlQuery, method, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res)\n );\n return;\n }\n\n // A single-read body is a zero-copy window DETACHED when this callback returns (HttpResponseWrapper.h): decode now.\n // A multi-read body was assembled in C++ and OWNERSHIP-TRANSFERRED to JS, so a microtask may decode it.\n if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {\n // a body-less request (every GET) skips the view and the decode altogether\n dispatchBody(fullBody.byteLength === 0 ? '' : Buffer.from(fullBody).toString());\n return;\n }\n // past one read's size → always the ownership-transferred path, so the buffer survives the microtask.\n // The zero-length check is a tripwire for an upstream change: fail loudly instead of parsing a neutered buffer.\n queueMicrotask(() => {\n if (state.replied) return;\n if (fullBody.byteLength === 0) {\n state.replied = true;\n const error = new FatalError({\n publicMessage: 'Internal Server Error',\n type: 'unknown-error',\n errorData: {reason: 'uws detached a multi-read body buffer (upstream behavior change) — report to mion'},\n });\n fatalFail(res, state, respHeaders, error);\n return;\n }\n dispatchBody(releaseAfterDecode(fullBody));\n });\n });\n}\n\n/** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.\n * `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.\n * Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */\nfunction releaseAfterDecode(fullBody: ArrayBuffer): string {\n const text = Buffer.from(fullBody).toString();\n try {\n (fullBody as ReleasableBuffer).transfer?.(0);\n } catch {\n // not transferable: the collector gets it instead, exactly as before\n }\n return text;\n}\n\n/** `transfer` is ES2024 and this package compiles against an ES2023 lib; optional because a runtime without it must work. */\ntype ReleasableBuffer = ArrayBuffer & {transfer?: (newByteLength?: number) => ArrayBuffer};\n\ninterface ReplyState {\n replied: boolean;\n aborted: boolean;\n /** RFC 9110: a HEAD answer has no body; one sent anyway is read as the start of the next answer */\n readonly head: boolean;\n}\n\n// only for http errors and unhandled route errors\nfunction fatalFail(res: HttpResponse, state: ReplyState, respHeaders: MionHeaders, error: RpcError<string>) {\n const routeResponse = getRouterFatalErrorResponse(error, respHeaders);\n reply(res, state, routeResponse);\n}\n\n/** The line is a pure function of the code, so it is built once per code rather than per response. */\nconst statusLines = new Map<number, string>();\nfunction statusLine(statusCode: number): string {\n const cached = statusLines.get(statusCode);\n if (cached !== undefined) return cached;\n const statusText = STATUS_CODES[statusCode];\n const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;\n statusLines.set(statusCode, line);\n return line;\n}\n\nfunction reply(res: HttpResponse, state: ReplyState, mionResp: MionResponse) {\n // uWS freed the response when the client left; touching it would crash\n if (state.aborted) return;\n const headers = mionResp.headers as RecordResponseHeaders;\n\n // serialized BEFORE the cork: uWS warns a cork buffer must not be held across event loop iterations\n const payload = JSON.stringify(mionResp.body);\n // uWS writes a `connection: close` header but keeps the socket open, so the close is asked for here\n const close = headers.get('connection')?.toLowerCase() === 'close';\n\n // cork makes one syscall; uWS headers are write-only and must all precede end().\n // content-length is skipped: uWS writes its own, and a duplicate corrupts the response.\n // the header store refused any CR, LF or NUL when it was set\n const write = (name: string, value: string) => res.writeHeader(name, value);\n res.cork(() => {\n res.writeStatus(statusLine(mionResp.statusCode));\n headers.forEach((name, value) => {\n if (name === 'content-length' || name === 'content-type') return;\n if (typeof value === 'string') write(name, value);\n // RFC 6265 forbids folding set-cookie; uWS writes a repeated name as its own line\n else if (name === 'set-cookie') for (const cookie of value) write(name, cookie);\n else write(name, value.join(', '));\n });\n // last, and the store's own skipped above: uWS appends a repeated name instead of replacing it\n res.writeHeader('content-type', mionResp.contentType);\n\n if (state.head) res.endWithoutBody(Buffer.byteLength(payload, 'utf8'), close);\n else res.end(payload, close);\n });\n releaseHeaders(headers);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mionjs/platform-uws",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "mion HTTP Server on uWebSockets.js for maximum performance.",
6
6
  "keywords": [
@@ -22,7 +22,6 @@
22
22
  "exports": {
23
23
  ".": {
24
24
  "types": "./.dist/esm/index.d.ts",
25
- "require": "./.dist/cjs/index.cjs",
26
25
  "default": "./.dist/esm/index.js"
27
26
  }
28
27
  },
@@ -46,12 +45,12 @@
46
45
  },
47
46
  "comment:engines": "Inherited constraint from @mionjs/bin-uws: uWebSockets.js ships prebuilt binaries only for these Node majors.",
48
47
  "engines": {
49
- "node": ">=22 <23 || >=24 <25 || >=26 <27"
48
+ "node": ">=24 <25 || >=26 <27"
50
49
  },
51
50
  "dependencies": {
52
- "@mionjs/core": "0.13.2",
53
- "@mionjs/router": "0.13.2",
54
- "@mionjs/bin-uws": "0.13.2"
51
+ "@mionjs/core": "0.14.0",
52
+ "@mionjs/router": "0.14.0",
53
+ "@mionjs/bin-uws": "0.14.0"
55
54
  },
56
55
  "scripts": {
57
56
  "typecheck:test": "tsc -p tsconfig.json --noEmit",
@@ -59,7 +58,6 @@
59
58
  "dev": "vite build --watch",
60
59
  "dev:test": "vitest watch",
61
60
  "lint": "eslint src",
62
- "format": "prettier --write src/**/*.ts",
63
61
  "build": "vite build",
64
62
  "clean": "rimraf .dist .coverage .mion",
65
63
  "fresh-start": "pnpm run clean && rimraf node_modules"
@@ -1,8 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_src_constants = require("./src/constants.cjs");
3
- const require_src_uwsHttp = require("./src/uwsHttp.cjs");
4
- exports.DEFAULT_UWS_HTTP_OPTIONS = require_src_constants.DEFAULT_UWS_HTTP_OPTIONS;
5
- exports.resetUwsHttpOpts = require_src_uwsHttp.resetUwsHttpOpts;
6
- exports.setUwsHttpOpts = require_src_uwsHttp.setUwsHttpOpts;
7
- exports.startUwsServer = require_src_uwsHttp.startUwsServer;
8
- exports.uwsRequestHandler = require_src_uwsHttp.uwsRequestHandler;
@@ -1,4 +0,0 @@
1
- export * from './src/uwsHttp.ts';
2
- export * from './src/types.ts';
3
- export * from './src/constants.ts';
4
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"type":"commonjs"}
@@ -1,11 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/constants.ts
3
- var DEFAULT_UWS_HTTP_OPTIONS = {
4
- port: 80,
5
- /** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */
6
- maxBodySize: require("@mionjs/core").DEFAULT_MAX_BODY_SIZE
7
- };
8
- //#endregion
9
- exports.DEFAULT_UWS_HTTP_OPTIONS = DEFAULT_UWS_HTTP_OPTIONS;
10
-
11
- //# sourceMappingURL=constants.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants.cjs","names":[],"sources":["../../../src/constants.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {DEFAULT_MAX_BODY_SIZE} from '@mionjs/core';\nimport {UwsHttpOptions} from './types.ts';\n\nexport const DEFAULT_UWS_HTTP_OPTIONS: UwsHttpOptions = {\n port: 80,\n /** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */\n maxBodySize: DEFAULT_MAX_BODY_SIZE,\n};\n"],"mappings":";;AAUA,IAAa,2BAA2C;CACtD,MAAM;;CAEN,mCAAa,EAAA;AACf"}
@@ -1,3 +0,0 @@
1
- import { UwsHttpOptions } from './types.ts';
2
- export declare const DEFAULT_UWS_HTTP_OPTIONS: UwsHttpOptions;
3
- //# sourceMappingURL=constants.d.ts.map
@@ -1,16 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let _mionjs_router = require("@mionjs/router");
3
- //#region src/headers.ts
4
- /** uWS headers are readable only synchronously inside the handler, so snapshot them; uWS already lower-cases the names. */
5
- function headersFromUwsRequest(req) {
6
- const record = {};
7
- req.forEach((name, value) => {
8
- const previous = record[name];
9
- record[name] = typeof previous === "string" ? `${previous}, ${value}` : value;
10
- });
11
- return (0, _mionjs_router.headersFromRecord)(record, true);
12
- }
13
- //#endregion
14
- exports.headersFromUwsRequest = headersFromUwsRequest;
15
-
16
- //# sourceMappingURL=headers.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"headers.cjs","names":[],"sources":["../../../src/headers.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {MionHeaders, headersFromRecord} from '@mionjs/router';\nimport type {HttpRequest} from '@mionjs/bin-uws';\n\n/** uWS headers are readable only synchronously inside the handler, so snapshot them; uWS already lower-cases the names. */\nexport function headersFromUwsRequest(req: HttpRequest): MionHeaders {\n const record: Record<string, string> = {};\n req.forEach((name, value) => {\n // a string check, not undefined: a `constructor` header would otherwise find Object.prototype's function\n const previous = record[name];\n record[name] = typeof previous === 'string' ? `${previous}, ${value}` : value;\n });\n return headersFromRecord(record, true);\n}\n"],"mappings":";;;;AAWA,SAAgB,sBAAsB,KAA+B;CACnE,MAAM,SAAiC,CAAC;CACxC,IAAI,SAAS,MAAM,UAAU;EAE3B,MAAM,WAAW,OAAO;EACxB,OAAO,QAAQ,OAAO,aAAa,WAAW,GAAG,SAAS,IAAI,UAAU;CAC1E,CAAC;CACD,QAAA,GAAA,eAAA,mBAAyB,QAAQ,IAAI;AACvC"}
@@ -1,4 +0,0 @@
1
- import { MionHeaders } from '@mionjs/router';
2
- import { HttpRequest } from '@mionjs/bin-uws';
3
- export declare function headersFromUwsRequest(req: HttpRequest): MionHeaders;
4
- //# sourceMappingURL=headers.d.ts.map
File without changes
@@ -1,8 +0,0 @@
1
- import { AppOptions } from '@mionjs/bin-uws';
2
- export interface UwsHttpOptions {
3
- port: number;
4
- ssl?: AppOptions;
5
- maxBodySize: number;
6
- maxBodySizeCap?: number;
7
- }
8
- //# sourceMappingURL=types.d.ts.map
@@ -1,224 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_src_constants = require("./constants.cjs");
3
- const require_src_headers = require("./headers.cjs");
4
- let _mionjs_router = require("@mionjs/router");
5
- let http = require("http");
6
- let _mionjs_bin_uws = require("@mionjs/bin-uws");
7
- let _mionjs_core = require("@mionjs/core");
8
- //#region src/uwsHttp.ts
9
- var httpOptions = { ...require_src_constants.DEFAULT_UWS_HTTP_OPTIONS };
10
- var UWS_MAX_SINGLE_READ = 524288;
11
- function resetUwsHttpOpts() {
12
- httpOptions = { ...require_src_constants.DEFAULT_UWS_HTTP_OPTIONS };
13
- (0, _mionjs_router.resetRouter)();
14
- }
15
- function setUwsHttpOpts(options) {
16
- httpOptions = {
17
- ...httpOptions,
18
- ...options
19
- };
20
- return httpOptions;
21
- }
22
- /** The platform config the router publishes: everything but the TLS file paths. */
23
- function serializablePlatformConfig() {
24
- const { ssl: _ssl, ...serializableConfig } = httpOptions;
25
- return serializableConfig;
26
- }
27
- async function startUwsServer(options) {
28
- const isTest = (0, _mionjs_core.getENV)("NODE_ENV") === "test";
29
- if (options) setUwsHttpOpts(options);
30
- if ((0, _mionjs_router.hostOwnsSocket)()) throw new Error("@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.");
31
- const url = `${httpOptions.ssl ? "https" : "http"}://localhost${httpOptions.port !== 80 ? `:${httpOptions.port}` : ""}`;
32
- if (!isTest) console.log(`mion uws server running on ${url}`, {
33
- port: httpOptions.port,
34
- httpOptions
35
- });
36
- const uws = (0, _mionjs_bin_uws.loadUws)();
37
- const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();
38
- app.any("/*", uwsRequestHandler);
39
- return new Promise((resolve, reject) => {
40
- app.listen(httpOptions.port, (listenSocket) => {
41
- if (!listenSocket) {
42
- reject(/* @__PURE__ */ new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));
43
- return;
44
- }
45
- (0, _mionjs_router.setPlatformConfig)(serializablePlatformConfig());
46
- let closed = false;
47
- const close = () => {
48
- if (closed) return;
49
- closed = true;
50
- uws.us_listen_socket_close(listenSocket);
51
- };
52
- const server = {
53
- app,
54
- listenSocket,
55
- close
56
- };
57
- const shutdownHandler = function() {
58
- if (!isTest) console.log(`Shutting down mion server on ${url}`);
59
- close();
60
- process.exit(0);
61
- };
62
- process.on("SIGINT", shutdownHandler);
63
- process.on("SIGTERM", shutdownHandler);
64
- resolve(server);
65
- });
66
- });
67
- }
68
- function drainRequestBody(res) {
69
- res.onData(() => {});
70
- }
71
- function uwsRequestHandler(res, req) {
72
- const state = {
73
- replied: false,
74
- aborted: false
75
- };
76
- const path = req.getUrl();
77
- const query = req.getQuery();
78
- const urlQuery = query === "" ? void 0 : query;
79
- const reqHeaders = require_src_headers.headersFromUwsRequest(req);
80
- const respHeaders = (0, _mionjs_router.getDefaultHeaders)();
81
- res.onAborted(() => {
82
- state.aborted = true;
83
- });
84
- const rawRequest = {
85
- path,
86
- urlQuery,
87
- headers: reqHeaders
88
- };
89
- let chain;
90
- try {
91
- chain = (0, _mionjs_router.resolveExecutionChain)(path, urlQuery, rawRequest);
92
- } catch (err) {
93
- drainRequestBody(res);
94
- state.replied = true;
95
- fatalFail(res, state, respHeaders, (0, _mionjs_router.toRpcError)(err));
96
- return;
97
- }
98
- const dispatchBody = (rawBody, readQueryBody) => {
99
- let reqRawBody = rawBody;
100
- let reqBodyType = _mionjs_core.SerializerModes.stringifyJson;
101
- try {
102
- const queryBody = readQueryBody ? (0, _mionjs_router.decodeQueryBody)(urlQuery, reqRawBody || void 0) : void 0;
103
- if (queryBody) {
104
- reqRawBody = queryBody.rawBody;
105
- reqBodyType = queryBody.bodyType;
106
- }
107
- } catch (err) {
108
- state.replied = true;
109
- fatalFail(res, state, respHeaders, err);
110
- return;
111
- }
112
- const context = (0, _mionjs_router.createContextFromChain)(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);
113
- let dispatched;
114
- try {
115
- dispatched = (0, _mionjs_router.dispatchMaybeSync)(context, rawRequest, res);
116
- } catch (err) {
117
- failWith(err);
118
- return;
119
- }
120
- if (dispatched instanceof Promise) answerWith(dispatched);
121
- else answer(dispatched);
122
- };
123
- const answer = (mionResponse) => {
124
- if (state.replied) return;
125
- state.replied = true;
126
- try {
127
- reply(res, state, mionResponse);
128
- } catch {}
129
- };
130
- const failWith = (err) => {
131
- if (state.replied) return;
132
- state.replied = true;
133
- fatalFail(res, state, respHeaders, (0, _mionjs_router.toRpcError)(err));
134
- };
135
- const answerWith = (dispatched) => {
136
- dispatched.then(answer).catch(failWith);
137
- };
138
- if (!chain.readsBody) {
139
- drainRequestBody(res);
140
- dispatchBody("", false);
141
- return;
142
- }
143
- res.collectBody(chain.maxBodySize, (fullBody) => {
144
- if (state.replied) return;
145
- if (fullBody === null) {
146
- answerWith((0, _mionjs_router.dispatchPlatformError)(chain, path, urlQuery, (0, _mionjs_router.requestPayloadTooLarge)(), reqHeaders, respHeaders, rawRequest, res));
147
- return;
148
- }
149
- if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {
150
- dispatchBody(fullBody.byteLength === 0 ? "" : Buffer.from(fullBody).toString(), true);
151
- return;
152
- }
153
- queueMicrotask(() => {
154
- if (state.replied) return;
155
- if (fullBody.byteLength === 0) {
156
- state.replied = true;
157
- fatalFail(res, state, respHeaders, new _mionjs_core.FatalError({
158
- publicMessage: "Internal Server Error",
159
- type: "unknown-error",
160
- errorData: { reason: "uws detached a multi-read body buffer (upstream behavior change) — report to mion" }
161
- }));
162
- return;
163
- }
164
- dispatchBody(releaseAfterDecode(fullBody), true);
165
- });
166
- });
167
- }
168
- /** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.
169
- * `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.
170
- * Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */
171
- function releaseAfterDecode(fullBody) {
172
- const text = Buffer.from(fullBody).toString();
173
- try {
174
- fullBody.transfer?.(0);
175
- } catch {}
176
- return text;
177
- }
178
- function fatalFail(res, state, respHeaders, error) {
179
- reply(res, state, (0, _mionjs_router.getRouterFatalErrorResponse)(error, respHeaders));
180
- }
181
- function isHeaderSafe(text) {
182
- for (let i = 0; i < text.length; i++) {
183
- const code = text.charCodeAt(i);
184
- if (code === 13 || code === 10 || code === 0) return false;
185
- }
186
- return true;
187
- }
188
- /** The line is a pure function of the code, so it is built once per code rather than per response. */
189
- var statusLines = /* @__PURE__ */ new Map();
190
- function statusLine(statusCode) {
191
- const cached = statusLines.get(statusCode);
192
- if (cached !== void 0) return cached;
193
- const statusText = http.STATUS_CODES[statusCode];
194
- const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;
195
- statusLines.set(statusCode, line);
196
- return line;
197
- }
198
- function reply(res, state, mionResp) {
199
- if (state.aborted) return;
200
- const headers = mionResp.headers;
201
- const payload = JSON.stringify(mionResp.body);
202
- const write = (name, value) => {
203
- if (isHeaderSafe(value)) res.writeHeader(name, value);
204
- };
205
- res.cork(() => {
206
- res.writeStatus(statusLine(mionResp.statusCode));
207
- headers.forEach((name, value) => {
208
- if (name === "content-length" || name === "content-type" || !isHeaderSafe(name)) return;
209
- if (typeof value === "string") write(name, value);
210
- else if (name === "set-cookie") for (const cookie of value) write(name, cookie);
211
- else write(name, value.join(", "));
212
- });
213
- res.writeHeader("content-type", mionResp.contentType);
214
- res.end(payload);
215
- });
216
- (0, _mionjs_router.releaseHeaders)(headers);
217
- }
218
- //#endregion
219
- exports.resetUwsHttpOpts = resetUwsHttpOpts;
220
- exports.setUwsHttpOpts = setUwsHttpOpts;
221
- exports.startUwsServer = startUwsServer;
222
- exports.uwsRequestHandler = uwsRequestHandler;
223
-
224
- //# sourceMappingURL=uwsHttp.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"uwsHttp.cjs","names":[],"sources":["../../../src/uwsHttp.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {\n dispatchMaybeSync,\n dispatchPlatformError,\n resolveExecutionChain,\n createContextFromChain,\n getRouterFatalErrorResponse,\n toRpcError,\n resetRouter,\n decodeQueryBody,\n setPlatformConfig,\n getDefaultHeaders,\n releaseHeaders,\n requestPayloadTooLarge,\n hostOwnsSocket,\n} from '@mionjs/router';\nimport {STATUS_CODES} from 'http';\nimport {loadUws} from '@mionjs/bin-uws';\nimport type {HttpRequest, HttpResponse, TemplatedApp, us_listen_socket} from '@mionjs/bin-uws';\nimport {DEFAULT_UWS_HTTP_OPTIONS} from './constants.ts';\nimport type {UwsHttpOptions} from './types.ts';\nimport type {MethodsExecutionChain, MionHeaders, MionResponse, RecordResponseHeaders} from '@mionjs/router';\nimport {getENV, SerializerModes} from '@mionjs/core';\nimport type {SerializerCode} from '@mionjs/core';\nimport {RpcError, FatalError} from '@mionjs/core';\nimport {headersFromUwsRequest} from './headers.ts';\n\n// ############# PRIVATE STATE #############\n\nlet httpOptions: Readonly<UwsHttpOptions> = {...DEFAULT_UWS_HTTP_OPTIONS};\n\n// The most bytes one uWS socket read can deliver: uSockets' LIBUS_RECV_BUFFER_LENGTH (512 KiB) at the tag pinned in\n// packages/bin-uws. A LARGER body cannot be single-read, which is what makes the zero-copy branch below safe (a\n// detachment tripwire guards it at runtime). Re-verify against uSockets on a tag bump.\nconst UWS_MAX_SINGLE_READ = 524288;\n\n/** The running server: the uWS app plus the socket handle listen() produced. */\nexport interface UwsServer {\n app: TemplatedApp;\n listenSocket: us_listen_socket;\n close(): void;\n}\n\n// ############# PUBLIC METHODS #############\n\nexport function resetUwsHttpOpts() {\n httpOptions = {...DEFAULT_UWS_HTTP_OPTIONS};\n resetRouter();\n}\n\nexport function setUwsHttpOpts(options?: Partial<UwsHttpOptions>) {\n httpOptions = {\n ...httpOptions,\n ...options,\n };\n\n return httpOptions;\n}\n\n/** The platform config the router publishes: everything but the TLS file paths. */\nfunction serializablePlatformConfig(): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {ssl: _ssl, ...serializableConfig} = httpOptions;\n return serializableConfig;\n}\n\nexport async function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer> {\n const isTest = getENV('NODE_ENV') === 'test';\n\n if (options) setUwsHttpOpts(options);\n // uWS is its own C++ event loop and owns its listen socket, so it cannot mount on a host node server.\n if (hostOwnsSocket()) {\n throw new Error(\n '@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen ' +\n 'socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.'\n );\n }\n const protocol = httpOptions.ssl ? 'https' : 'http';\n const port = httpOptions.port !== 80 ? `:${httpOptions.port}` : '';\n const url = `${protocol}://localhost${port}`;\n if (!isTest)\n console.log(`mion uws server running on ${url}`, {\n port: httpOptions.port,\n httpOptions,\n });\n\n const uws = loadUws();\n const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();\n app.any('/*', uwsRequestHandler);\n\n return new Promise<UwsServer>((resolve, reject) => {\n app.listen(httpOptions.port, (listenSocket) => {\n if (!listenSocket) {\n reject(new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));\n return;\n }\n\n setPlatformConfig(serializablePlatformConfig());\n\n let closed = false;\n const close = () => {\n if (closed) return;\n closed = true;\n uws.us_listen_socket_close(listenSocket);\n };\n const server: UwsServer = {app, listenSocket, close};\n\n const shutdownHandler = function () {\n if (!isTest) console.log(`Shutting down mion server on ${url}`);\n close();\n process.exit(0);\n };\n process.on('SIGINT', shutdownHandler);\n process.on('SIGTERM', shutdownHandler);\n\n resolve(server);\n });\n });\n}\n\n// ############# PRIVATE METHODS #############\n\n// uWS refreshes a socket's idle timeout only from inside a body-data callback, and runs it only when a reader is registered.\n// Without one, a request answered before its body finished arriving is closed mid-upload, so drop every chunk instead.\nfunction drainRequestBody(res: HttpResponse) {\n res.onData(() => {});\n}\n\n// exported for tests and for mounting on a hand-built uWS app; NOT a middleware handler (see startUwsServer).\n// uWS contract: `req` is valid only synchronously here, so the async dispatch snapshots it before the first await.\nexport function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void {\n const state = {replied: false, aborted: false};\n // Everything read from `req` happens HERE, synchronously.\n const path = req.getUrl();\n const query = req.getQuery();\n const urlQuery = query === '' ? undefined : query;\n const reqHeaders = headersFromUwsRequest(req);\n\n const respHeaders = getDefaultHeaders();\n\n // must be registered before any async work: after a disconnect uWS frees the response, and touching it would crash\n res.onAborted(() => {\n state.aborted = true;\n });\n\n // route resolved BEFORE the body, synchronously: the native read stops at the chain's limit, and a late context stays GC-cheap.\n // The raw request object is built once: the one a pathTransform reads and the one the handlers see.\n const rawRequest = {path, urlQuery, headers: reqHeaders};\n let chain: MethodsExecutionChain;\n try {\n chain = resolveExecutionChain(path, urlQuery, rawRequest);\n } catch (err) {\n drainRequestBody(res);\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n return;\n }\n\n const dispatchBody = (rawBody: string, readQueryBody: boolean) => {\n let reqRawBody: any = rawBody;\n let reqBodyType: SerializerCode = SerializerModes.stringifyJson;\n // a throw here runs inside uWS' native callback (or a microtask): it must become a response\n try {\n const queryBody = readQueryBody ? decodeQueryBody(urlQuery, reqRawBody || undefined) : undefined;\n if (queryBody) {\n reqRawBody = queryBody.rawBody;\n reqBodyType = queryBody.bodyType;\n }\n } catch (err) {\n state.replied = true;\n fatalFail(res, state, respHeaders, err as RpcError<string>);\n return;\n }\n\n const context = createContextFromChain(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);\n let dispatched: MionResponse | Promise<MionResponse>;\n try {\n dispatched = dispatchMaybeSync(context, rawRequest, res);\n } catch (err) {\n failWith(err);\n return;\n }\n // an all-sync chain answers inside uWS' own callback, with no promise in between\n if (dispatched instanceof Promise) answerWith(dispatched);\n else answer(dispatched);\n };\n\n const answer = (mionResponse: MionResponse) => {\n if (state.replied) return;\n state.replied = true;\n try {\n reply(res, state, mionResponse);\n } catch {\n // already marked replied: a failed write has nothing left to answer with\n }\n };\n\n const failWith = (err: unknown) => {\n if (state.replied) return;\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n };\n\n const answerWith = (dispatched: Promise<MionResponse>) => {\n dispatched.then(answer).catch(failWith);\n };\n\n // collectBody assembles the body natively (uWS' onDataV2 preallocates) and calls back ONCE, with null past maxSize,\n // which is exactly the maxBodySize contract. A not-found chain has no route to feed: its body is dropped as it arrives.\n if (!chain.readsBody) {\n drainRequestBody(res);\n dispatchBody('', false);\n return;\n }\n\n res.collectBody(chain.maxBodySize, (fullBody) => {\n if (state.replied) return;\n if (fullBody === null) {\n // the route resolved, so the refusal still runs the chain's alwaysRun members\n answerWith(\n dispatchPlatformError(chain, path, urlQuery, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res)\n );\n return;\n }\n\n // collectBody has two paths (verified in the pinned tag's HttpResponseWrapper.h and by test): a single-read body is a\n // zero-copy window into uWS' receive buffer, DETACHED when this callback returns, so it must be decoded synchronously\n // here; a multi-read body was assembled in C++ and OWNERSHIP-TRANSFERRED to JS, so it can be decoded from a microtask.\n if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {\n // a body-less request (every GET) skips the view and the decode altogether\n dispatchBody(fullBody.byteLength === 0 ? '' : Buffer.from(fullBody).toString(), true);\n return;\n }\n // bigger than one read can deliver → guaranteed the ownership-transferred path, so the buffer survives the microtask.\n // The zero-length check is a tripwire for an upstream change: fail loudly instead of parsing a neutered buffer.\n queueMicrotask(() => {\n if (state.replied) return;\n if (fullBody.byteLength === 0) {\n state.replied = true;\n const error = new FatalError({\n publicMessage: 'Internal Server Error',\n type: 'unknown-error',\n errorData: {reason: 'uws detached a multi-read body buffer (upstream behavior change) — report to mion'},\n });\n fatalFail(res, state, respHeaders, error);\n return;\n }\n dispatchBody(releaseAfterDecode(fullBody), true);\n });\n });\n}\n\n/** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.\n * `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.\n * Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */\nfunction releaseAfterDecode(fullBody: ArrayBuffer): string {\n const text = Buffer.from(fullBody).toString();\n try {\n (fullBody as ReleasableBuffer).transfer?.(0);\n } catch {\n // not transferable: the collector gets it instead, exactly as before\n }\n return text;\n}\n\n/** `transfer` is ES2024 and this package compiles against an ES2023 lib; optional because a runtime without it must work. */\ntype ReleasableBuffer = ArrayBuffer & {transfer?: (newByteLength?: number) => ArrayBuffer};\n\n// only called when there is an http error or weird unhandled route errors\nfunction fatalFail(res: HttpResponse, state: {aborted: boolean}, respHeaders: MionHeaders, error: RpcError<string>) {\n const routeResponse = getRouterFatalErrorResponse(error, respHeaders);\n reply(res, state, routeResponse);\n}\n\nfunction isHeaderSafe(text: string): boolean {\n // one pass, no allocation: a CR, LF or NUL anywhere makes the header unsafe to write\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (code === 13 || code === 10 || code === 0) return false;\n }\n return true;\n}\n\n/** The line is a pure function of the code, so it is built once per code rather than per response. */\nconst statusLines = new Map<number, string>();\nfunction statusLine(statusCode: number): string {\n const cached = statusLines.get(statusCode);\n if (cached !== undefined) return cached;\n const statusText = STATUS_CODES[statusCode];\n const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;\n statusLines.set(statusCode, line);\n return line;\n}\n\nfunction reply(res: HttpResponse, state: {aborted: boolean}, mionResp: MionResponse) {\n // uWS freed the response when the client left; touching it would crash\n if (state.aborted) return;\n const headers = mionResp.headers as RecordResponseHeaders;\n\n // serialized BEFORE the cork: uWS warns a cork buffer must not be held across event loop iterations\n const payload = JSON.stringify(mionResp.body);\n\n // cork makes one syscall; uWS headers are write-only and must all precede end().\n // content-length is skipped: uWS writes its own, and a duplicate corrupts the response.\n // uWS writes headers unchecked, so an echoed CR or LF would be header injection: dropped\n const write = (name: string, value: string) => {\n if (isHeaderSafe(value)) res.writeHeader(name, value);\n };\n res.cork(() => {\n res.writeStatus(statusLine(mionResp.statusCode));\n headers.forEach((name, value) => {\n if (name === 'content-length' || name === 'content-type' || !isHeaderSafe(name)) return;\n if (typeof value === 'string') write(name, value);\n // RFC 6265 forbids folding set-cookie; uWS writes a repeated name as its own line\n else if (name === 'set-cookie') for (const cookie of value) write(name, cookie);\n else write(name, value.join(', '));\n });\n // last, and the store's own skipped above: uWS appends a repeated name instead of replacing it\n res.writeHeader('content-type', mionResp.contentType);\n\n res.end(payload);\n });\n releaseHeaders(headers);\n}\n"],"mappings":"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"}
@@ -1,12 +0,0 @@
1
- import { HttpRequest, HttpResponse, TemplatedApp, us_listen_socket } from '@mionjs/bin-uws';
2
- import { UwsHttpOptions } from './types.ts';
3
- export interface UwsServer {
4
- app: TemplatedApp;
5
- listenSocket: us_listen_socket;
6
- close(): void;
7
- }
8
- export declare function resetUwsHttpOpts(): void;
9
- export declare function setUwsHttpOpts(options?: Partial<UwsHttpOptions>): Readonly<UwsHttpOptions>;
10
- export declare function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer>;
11
- export declare function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void;
12
- //# sourceMappingURL=uwsHttp.d.ts.map