dx-server 0.13.0 → 0.14.0-alpha.1

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.
Files changed (46) hide show
  1. package/README.md +397 -265
  2. package/lib/body.js +1 -1
  3. package/lib/body.js.map +1 -0
  4. package/lib/bodyHelpers.js +22 -9
  5. package/lib/bodyHelpers.js.map +1 -0
  6. package/lib/dx.d.ts +1 -1
  7. package/lib/dx.js +12 -6
  8. package/lib/dx.js.map +1 -0
  9. package/lib/dxHelpers.d.ts +2 -1
  10. package/lib/dxHelpers.js +105 -87
  11. package/lib/dxHelpers.js.map +1 -0
  12. package/lib/helpers.js.map +1 -0
  13. package/lib/index.d.ts +1 -1
  14. package/lib/index.js +1 -1
  15. package/lib/index.js.map +1 -0
  16. package/lib/logger.d.ts +3 -2
  17. package/lib/logger.js +54 -46
  18. package/lib/logger.js.map +1 -0
  19. package/lib/router.js +5 -5
  20. package/lib/router.js.map +1 -0
  21. package/lib/static.js +4 -3
  22. package/lib/static.js.map +1 -0
  23. package/lib/staticHelpers.d.ts +5 -1
  24. package/lib/staticHelpers.js +150 -134
  25. package/lib/staticHelpers.js.map +1 -0
  26. package/lib/stream.d.ts +1 -1
  27. package/lib/stream.js +11 -5
  28. package/lib/stream.js.map +1 -0
  29. package/lib/vendors/contentType.js +7 -30
  30. package/lib/vendors/contentType.js.map +1 -0
  31. package/lib/vendors/etag.d.ts +2 -2
  32. package/lib/vendors/etag.js +15 -25
  33. package/lib/vendors/etag.js.map +1 -0
  34. package/lib/vendors/fresh.js +10 -17
  35. package/lib/vendors/fresh.js.map +1 -0
  36. package/lib/vendors/mime.js +4 -4
  37. package/lib/vendors/mime.js.map +1 -0
  38. package/lib/vendors/mimeDb.d.ts +2544 -2544
  39. package/lib/vendors/mimeDb.js +7100 -7079
  40. package/lib/vendors/mimeDb.js.map +1 -0
  41. package/lib/vendors/mimeScore.js +10 -11
  42. package/lib/vendors/mimeScore.js.map +1 -0
  43. package/lib/vendors/rangeParser.d.ts +2 -10
  44. package/lib/vendors/rangeParser.js +16 -29
  45. package/lib/vendors/rangeParser.js.map +1 -0
  46. package/package.json +32 -27
package/lib/body.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getReq, makeDxContext } from './dx.js';
2
- import { bufferFromReq, jsonFromReq, queryFromReq, rawFromReq, textFromReq, urlEncodedFromReq } from './bodyHelpers.js';
2
+ import { bufferFromReq, jsonFromReq, queryFromReq, rawFromReq, textFromReq, urlEncodedFromReq, } from './bodyHelpers.js';
3
3
  export const getBuffer = makeDxContext((options) => bufferFromReq(getReq(), options));
4
4
  export const getJson = makeDxContext((options) => jsonFromReq(getReq(), options));
5
5
  export const getRaw = makeDxContext((options) => rawFromReq(getReq(), options));
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAE,aAAa,EAAC,MAAM,SAAS,CAAA;AAC7C,OAAO,EAEN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AAEzB,MAAM,CAAC,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAClH,MAAM,CAAC,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAC9G,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAC5G,MAAM,CAAC,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAC9G,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CACnF,iBAAiB,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CACpC,CAAA;AACD,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,OAAoC,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;AAEhH,wBAAwB;AACxB,mCAAmC","sourcesContent":["import {getReq, makeDxContext} from './dx.js'\nimport {\n\ttype BufferBodyOptions,\n\tbufferFromReq,\n\tjsonFromReq,\n\tqueryFromReq,\n\trawFromReq,\n\ttextFromReq,\n\turlEncodedFromReq,\n} from './bodyHelpers.js'\n\nexport const getBuffer = makeDxContext((options?: Partial<BufferBodyOptions>) => bufferFromReq(getReq(), options))\nexport const getJson = makeDxContext((options?: Partial<BufferBodyOptions>) => jsonFromReq(getReq(), options))\nexport const getRaw = makeDxContext((options?: Partial<BufferBodyOptions>) => rawFromReq(getReq(), options))\nexport const getText = makeDxContext((options?: Partial<BufferBodyOptions>) => textFromReq(getReq(), options))\nexport const getUrlEncoded = makeDxContext((options?: Partial<BufferBodyOptions>) =>\n\turlEncodedFromReq(getReq(), options),\n)\nexport const getQuery = makeDxContext((options?: Partial<BufferBodyOptions>) => queryFromReq(getReq(), options))\n\n// to getFile use busboy\n// https://github.com/mscdex/busboy\n"]}
@@ -17,17 +17,28 @@ export async function bufferFromReq(req, options) {
17
17
  */
18
18
  // https://github.com/jshttp/type-is/blob/cdcfe23e9833872e425b0aaf71ca0311373b6116/index.js#L92
19
19
  const contentLengthParsed = parseInt(req.headers['content-length'] ?? '', 10);
20
- if (req.headers['transfer-encoding'] === undefined
21
- && isNaN(contentLengthParsed))
20
+ if (req.headers['transfer-encoding'] === undefined && isNaN(contentLengthParsed))
22
21
  return;
23
22
  const contentLength = isNaN(contentLengthParsed) ? undefined : contentLengthParsed;
24
23
  // read
25
24
  const encoding = (req.headers['content-encoding'] ?? 'identity').toLowerCase();
26
25
  const stream = getContentStream(req, encoding);
27
- return await readStream(stream, {
28
- length: encoding === 'identity' ? contentLength : undefined,
29
- limit: bodyLimit,
30
- });
26
+ try {
27
+ return await readStream(stream, {
28
+ length: encoding === 'identity' ? contentLength : undefined,
29
+ limit: bodyLimit,
30
+ });
31
+ }
32
+ catch (e) {
33
+ // On rejection (e.g. body limit exceeded) tear down the decompressor and unpipe it
34
+ // from req. Otherwise req keeps feeding the decompressor and a zip-bomb keeps inflating
35
+ // long after the limit fired. req itself is left alive so error middleware can respond.
36
+ if (stream !== req) {
37
+ req.unpipe(stream);
38
+ stream.destroy();
39
+ }
40
+ throw e;
41
+ }
31
42
  }
32
43
  // if content-type is not as expected, return undefined
33
44
  function forceGetContentTypeParams(req, expected) {
@@ -45,7 +56,9 @@ function forceGetCharset(req, expected) {
45
56
  return;
46
57
  // assert charset per RFC 7159 sec 8.1
47
58
  const charset = parameters.charset?.toLowerCase() || 'utf-8';
48
- if (!charset.startsWith('utf-'))
59
+ // positive allowlist: utf-7 etc. would pass a `startsWith('utf-')` check but is not a valid
60
+ // Buffer encoding (and utf-7 is an XSS smuggling vector)
61
+ if (!['utf-8', 'utf8', 'utf-16le', 'utf16le'].includes(charset))
49
62
  throw new Error(`unsupported charset "${charset.toUpperCase()}"`);
50
63
  return charset;
51
64
  }
@@ -78,12 +91,12 @@ export async function urlEncodedFromReq(req, options) {
78
91
  return;
79
92
  const buffer = await bufferFromReq(req, options);
80
93
  if (buffer) {
81
- return (bodyDefaultOptions.urlEncodedParser ?? options?.urlEncodedParser ?? defaultQueryParser)(buffer.toString(charset));
94
+ return (options?.urlEncodedParser ?? bodyDefaultOptions.urlEncodedParser ?? defaultQueryParser)(buffer.toString(charset));
82
95
  }
83
96
  }
84
97
  export function urlFromReq(req) {
85
98
  return new URL(req.url ?? '', 'https://example.com');
86
99
  }
87
100
  export function queryFromReq(req, options) {
88
- return (bodyDefaultOptions.queryParser ?? options?.queryParser ?? defaultQueryParser)(urlFromReq(req).searchParams.toString());
101
+ return (options?.queryParser ?? bodyDefaultOptions.queryParser ?? defaultQueryParser)(urlFromReq(req).searchParams.toString());
89
102
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bodyHelpers.js","sourceRoot":"","sources":["../src/bodyHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["import {IncomingMessage} from 'node:http'\nimport {getContentStream, readStream} from './stream.js'\nimport {parseContentType} from './vendors/contentType.js'\n\nexport interface BufferBodyOptions {\n\tbodyLimit: number\n\turlEncodedParser?(search: string): any\n\tqueryParser?(search: string): any\n}\n\nfunction defaultQueryParser(search: string) {\n\treturn Object.fromEntries(new URLSearchParams(search)) // support both leading ? and not\n}\n\nlet bodyDefaultOptions: BufferBodyOptions = {bodyLimit: 100 << 10} // 100kb\nexport function setBufferBodyDefaultOptions(options: Partial<BufferBodyOptions>) {\n\tbodyDefaultOptions = {...bodyDefaultOptions, ...options}\n}\n\nexport async function bufferFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst {bodyLimit} = {...bodyDefaultOptions, ...options}\n\t/**\n\t * Check if a request has a request body.\n\t * A request with a body __must__ either have `transfer-encoding`\n\t * or `content-length` headers set.\n\t * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3\n\t */\n\t// https://github.com/jshttp/type-is/blob/cdcfe23e9833872e425b0aaf71ca0311373b6116/index.js#L92\n\tconst contentLengthParsed = parseInt(req.headers['content-length'] ?? '', 10)\n\tif (req.headers['transfer-encoding'] === undefined && isNaN(contentLengthParsed)) return\n\tconst contentLength = isNaN(contentLengthParsed) ? undefined : contentLengthParsed\n\n\t// read\n\tconst encoding = (req.headers['content-encoding'] ?? 'identity').toLowerCase()\n\tconst stream = getContentStream(req, encoding)\n\ttry {\n\t\treturn await readStream(stream, {\n\t\t\tlength: encoding === 'identity' ? contentLength : undefined,\n\t\t\tlimit: bodyLimit,\n\t\t})\n\t} catch (e) {\n\t\t// On rejection (e.g. body limit exceeded) tear down the decompressor and unpipe it\n\t\t// from req. Otherwise req keeps feeding the decompressor and a zip-bomb keeps inflating\n\t\t// long after the limit fired. req itself is left alive so error middleware can respond.\n\t\tif (stream !== req) {\n\t\t\treq.unpipe(stream as NodeJS.WritableStream)\n\t\t\tstream.destroy()\n\t\t}\n\t\tthrow e\n\t}\n}\n\n// if content-type is not as expected, return undefined\nfunction forceGetContentTypeParams(req: IncomingMessage, expected: string) {\n\tconst contentTypeRaw = req.headers['content-type']\n\tif (!contentTypeRaw) return\n\tconst {mediaType, parameters} = parseContentType(contentTypeRaw)\n\tif (mediaType !== expected) return\n\n\treturn parameters\n}\nfunction forceGetCharset(req: IncomingMessage, expected: string) {\n\tconst parameters = forceGetContentTypeParams(req, expected)\n\tif (!parameters) return\n\t// assert charset per RFC 7159 sec 8.1\n\tconst charset = (parameters.charset?.toLowerCase() as BufferEncoding) || 'utf-8'\n\t// positive allowlist: utf-7 etc. would pass a `startsWith('utf-')` check but is not a valid\n\t// Buffer encoding (and utf-7 is an XSS smuggling vector)\n\tif (!['utf-8', 'utf8', 'utf-16le', 'utf16le'].includes(charset))\n\t\tthrow new Error(`unsupported charset \"${charset.toUpperCase()}\"`)\n\n\treturn charset\n}\n\nexport async function jsonFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst charset = forceGetCharset(req, 'application/json')\n\tif (!charset) return\n\tconst buffer = await bufferFromReq(req, options)\n\tif (buffer) {\n\t\tconst str = buffer.toString(charset)\n\t\treturn str ? JSON.parse(str) : undefined\n\t}\n}\n\nexport async function rawFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tif (!forceGetContentTypeParams(req, 'application/octet-stream')) return\n\treturn await bufferFromReq(req, options)\n}\n\nexport async function textFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst charset = forceGetCharset(req, 'text/plain')\n\tif (!charset) return\n\tconst buffer = await bufferFromReq(req, options)\n\tif (buffer) return buffer.toString(charset)\n}\n\nexport async function urlEncodedFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\tconst charset = forceGetCharset(req, 'application/x-www-form-urlencoded')\n\tif (!charset) return\n\tconst buffer = await bufferFromReq(req, options)\n\tif (buffer) {\n\t\treturn (bodyDefaultOptions.urlEncodedParser ?? options?.urlEncodedParser ?? defaultQueryParser)(\n\t\t\tbuffer.toString(charset),\n\t\t)\n\t}\n}\n\nexport function urlFromReq(req: IncomingMessage) {\n\treturn new URL(req.url ?? '', 'https://example.com')\n}\n\nexport function queryFromReq(req: IncomingMessage, options?: Partial<BufferBodyOptions>) {\n\treturn (bodyDefaultOptions.queryParser ?? options?.queryParser ?? defaultQueryParser)(\n\t\turlFromReq(req).searchParams.toString(),\n\t)\n}\n"]}
package/lib/dx.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare function dxServer(req: IncomingMessage, res: ServerResponse, opti
17
17
  disableEtag?: boolean;
18
18
  }): Chainable;
19
19
  export declare function getReq(): IncomingMessage;
20
- export declare function getRes(): ServerResponse;
20
+ export declare function getRes(): ServerResponse<IncomingMessage>;
21
21
  export declare function setText(text: string, { status }?: {
22
22
  status?: number;
23
23
  }): void;
package/lib/dx.js CHANGED
@@ -14,17 +14,19 @@ export function makeDxContext(maker) {
14
14
  return promiseMap.get(req);
15
15
  });
16
16
  Object.defineProperty(context, 'value', {
17
- get() { return valueMap.get(getReq()); },
17
+ get() {
18
+ return valueMap.get(getReq());
19
+ },
18
20
  set(value) {
19
21
  const req = getReq();
20
22
  promiseMap.set(req, Promise.resolve(value));
21
23
  valueMap.set(req, value);
22
- }
24
+ },
23
25
  });
24
- context.chain = ((...params) => (async (next) => {
26
+ context.chain = ((...params) => async (next) => {
25
27
  await context(...params);
26
28
  return next();
27
- }));
29
+ });
28
30
  context.set = (req, value) => {
29
31
  promiseMap.set(req, Promise.resolve(value));
30
32
  valueMap.set(req, value);
@@ -46,8 +48,12 @@ export function dxServer(req, res, options = {}) {
46
48
  // url: full url without server, protocol, port.
47
49
  // headers: if headers are repeated, they are joined by comma. Header names are lowercased.
48
50
  // rawHeaders: list of header name and value in a flat array. Case is preserved.
49
- export function getReq() { return requestStorage.getStore().req; }
50
- export function getRes() { return requestStorage.getStore().res; }
51
+ export function getReq() {
52
+ return requestStorage.getStore().req;
53
+ }
54
+ export function getRes() {
55
+ return requestStorage.getStore().res;
56
+ }
51
57
  export function setText(text, { status } = {}) {
52
58
  const res = getRes();
53
59
  const dx = dxContext.value;
package/lib/dx.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dx.js","sourceRoot":"","sources":["../src/dx.ts"],"names":[],"mappings":"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","sourcesContent":["import {Readable} from 'node:stream'\nimport type {IncomingMessage, ServerResponse} from 'node:http'\nimport {AsyncLocalStorage} from 'node:async_hooks'\nimport {type DxContext, writeRes} from './dxHelpers.js'\nimport type {SendFileOptions} from './staticHelpers.js'\n\nexport interface Chainable<R = any, Next = (...np: any[]) => any> {\n\t(next: Next): R\n}\n\nexport interface Context<T, Params extends any[] = any[], R = any, Next = (...np: any[]) => any> {\n\tvalue: Awaited<T> // can be undefined\n\tget(req: IncomingMessage): T\n\tset(req: IncomingMessage, value: T): void\n\t(...params: Params): Promise<T>\n\tchain(...params: Params): Chainable<R, Next>\n}\nexport function makeDxContext<T, Params extends any[] = any[], R = any, Next = (...np: any[]) => any>(\n\tmaker: (...params: Params) => T | Promise<T>,\n): Context<T, Params, R, Next> {\n\tconst promiseMap = new WeakMap<IncomingMessage, Promise<T>>()\n\tconst valueMap = new WeakMap<IncomingMessage, T>()\n\tconst context = ((...params: Params) => {\n\t\tconst req = getReq()\n\t\tif (!promiseMap.has(req))\n\t\t\tpromiseMap.set(\n\t\t\t\treq,\n\t\t\t\t(async () => {\n\t\t\t\t\tconst value = await maker(...params)\n\t\t\t\t\tvalueMap.set(req, value)\n\t\t\t\t\treturn value\n\t\t\t\t})(),\n\t\t\t)\n\t\treturn promiseMap.get(req)!\n\t}) as Context<T, Params, R, Next>\n\tObject.defineProperty(context, 'value', {\n\t\tget() {\n\t\t\treturn valueMap.get(getReq())\n\t\t},\n\t\tset(value) {\n\t\t\tconst req = getReq()\n\t\t\tpromiseMap.set(req, Promise.resolve(value))\n\t\t\tvalueMap.set(req, value)\n\t\t},\n\t})\n\tcontext.chain = ((...params: Params) =>\n\t\tasync (next: Next) => {\n\t\t\tawait context(...params)\n\t\t\treturn (next as (...args: any[]) => any)()\n\t\t}) as Context<T, Params, R, Next>['chain']\n\tcontext.set = (req, value) => {\n\t\tpromiseMap.set(req, Promise.resolve(value))\n\t\tvalueMap.set(req, value)\n\t}\n\tcontext.get = req => valueMap.get(req) as T\n\treturn context\n}\n\nconst requestStorage = new AsyncLocalStorage<{\n\treq: IncomingMessage\n\tres: ServerResponse\n}>()\nconst dxContext = makeDxContext<DxContext>(options => ({...options}) as DxContext)\nexport function dxServer(\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\toptions: {\n\t\tjsonBeautify?: boolean\n\t\tdisableEtag?: boolean\n\t} = {},\n): Chainable {\n\treturn async next => {\n\t\tdxContext.set(req, {...options} as DxContext)\n\t\tconst result = await requestStorage.run({req, res}, next)\n\t\tawait writeRes(req, res, dxContext.get(req))\n\t\treturn result\n\t}\n}\n\n// method: verb\n// url: full url without server, protocol, port.\n// headers: if headers are repeated, they are joined by comma. Header names are lowercased.\n// rawHeaders: list of header name and value in a flat array. Case is preserved.\nexport function getReq() {\n\treturn requestStorage.getStore()!.req\n}\nexport function getRes() {\n\treturn requestStorage.getStore()!.res\n}\n\nexport function setText(text: string, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = text\n\tdx.type = 'text'\n}\n\nexport function setEmpty({status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = undefined\n\tdx.type = 'empty'\n}\n\nexport function setHtml(html: string, opts: {status?: number} = {}) {\n\tsetText(html, opts)\n\tconst dx = dxContext.value\n\tdx.type = 'html'\n}\n\nexport function setFile(filePath: string, options?: SendFileOptions) {\n\tconst dx = dxContext.value\n\tdx.data = filePath\n\tdx.type = 'file'\n\tdx.options = options\n}\n\nexport function setBuffer(buffer: Buffer, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = buffer\n\tdx.type = 'buffer'\n}\n\nexport function setNodeStream(stream: Readable, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = stream\n\tdx.type = 'nodeStream'\n}\n\nexport function setWebStream(stream: ReadableStream, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tif (status) res.statusCode = status\n\tdx.data = stream\n\tdx.type = 'webStream'\n}\n\nexport function setJson(json: any, {status}: {status?: number} = {}) {\n\tconst res = getRes()\n\tif (status) res.statusCode = status\n\n\tconst dx = dxContext.value\n\tdx.data = json\n\tdx.type = 'json'\n}\n\nexport function setRedirect(url: string, status: 301 | 302) {\n\tconst res = getRes()\n\tconst dx = dxContext.value\n\tres.statusCode = status\n\tdx.data = url\n\tdx.type = 'redirect'\n}\n\n// for download, set content-disposition header\n// res.setHeader('Content-disposition', 'attachment; filename=my-movie.MOV')\n\n// res.setHeader('Content-type', 'video/quicktime')\n// fileStream.pipe(res)\n// or\n// send(req, filePath, options).pipe(res) // which will set content-type, content-length, and other cache related headers like staticHelpers.sendFile\n\n// implementing this require a strict validation for the type (attachment) and filename.\n// For example: express relies on this\n// https://github.com/jshttp/content-disposition/blob/1037e24e4790273da96645ad250061f39e77968c/index.js#L186\n// because in most applications, users can specify a simple filename which usually doesn't need to be validated.\n// we leave setDownload() implementation for users, for now.\n"]}
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { Readable } from 'node:stream';
3
+ import type { ReadableStream as WebReadableStream } from 'node:stream/web';
3
4
  import { type SendFileOptions } from './staticHelpers.js';
4
5
  export type DxContext = {
5
6
  charset?: BufferEncoding;
@@ -35,7 +36,7 @@ export type DxContext = {
35
36
  options: undefined;
36
37
  } | {
37
38
  type: 'webStream';
38
- data: ReadableStream;
39
+ data: WebReadableStream;
39
40
  options: undefined;
40
41
  } | {
41
42
  type: 'file';
package/lib/dxHelpers.js CHANGED
@@ -1,117 +1,135 @@
1
1
  import { Readable } from 'node:stream';
2
+ import { pipeline } from 'node:stream/promises';
2
3
  import { promisify } from 'node:util';
3
4
  import { entityTag, isFreshETag } from './vendors/etag.js';
4
5
  import { sendFileTrusted } from './staticHelpers.js';
5
6
  export async function writeRes(req, res, { type, data, charset, jsonBeautify, disableEtag, options }) {
6
- const setContentType = (contentType) => {
7
- if (res.headersSent || res.getHeader('content-type'))
8
- return;
9
- res.setHeader('content-type', `${contentType}${charset ? `; charset=${charset}` : ''}`);
10
- };
11
- let bufferOrStream;
7
+ if (res.headersSent)
8
+ return;
9
+ let buffer;
12
10
  switch (type) {
13
11
  case 'text':
14
- setContentType('text/plain');
15
12
  case 'html':
16
- setContentType('text/html');
17
- // shared with text
18
- bufferOrStream = Buffer.from(data ?? '', charset);
13
+ setContentType(type === 'html' ? 'text/html' : 'text/plain');
14
+ buffer = Buffer.from(data ?? '', charset);
19
15
  break;
20
16
  case 'buffer':
21
17
  setContentType('application/octet-stream');
22
- bufferOrStream = data ?? Buffer.from('', charset);
23
- break;
24
- case 'nodeStream':
25
- setContentType('application/octet-stream');
26
- bufferOrStream = data ?? Buffer.from('', charset);
27
- break;
28
- case 'webStream':
29
- setContentType('application/octet-stream');
30
- bufferOrStream = Readable.fromWeb(data ?? new ReadableStream());
18
+ buffer = data ?? Buffer.from('', charset);
31
19
  break;
32
20
  case 'json':
33
21
  setContentType('application/json');
34
- bufferOrStream = data === undefined
35
- ? Buffer.from('', charset)
36
- : Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data), charset);
22
+ buffer =
23
+ data === undefined
24
+ ? Buffer.from('', charset)
25
+ : Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data), charset);
37
26
  break;
38
- case 'redirect': // https://stackoverflow.com/a/8059718/1398479
39
- res.setHeader('location', data);
40
- bufferOrStream = Buffer.from('', charset);
27
+ case 'redirect':
28
+ case 'empty':
29
+ if (type === 'redirect')
30
+ res.setHeader('location', data);
31
+ buffer = Buffer.from('', charset);
41
32
  break;
33
+ // Streaming paths own res.end() themselves (via pipeline/sendFileTrusted) and must be
34
+ // awaited to fulfil the "chain resolves after flush" invariant.
35
+ case 'nodeStream':
36
+ case 'webStream':
37
+ if (!data) {
38
+ buffer = Buffer.from('', charset);
39
+ break;
40
+ }
42
41
  case 'file':
42
+ setContentType('application/octet-stream');
43
43
  try {
44
- await sendFileTrusted(req, res, data, options);
44
+ if (type === 'file')
45
+ await sendFileTrusted(req, res, data, options);
46
+ else if (type === 'nodeStream')
47
+ await pipeline(data, res);
48
+ else if (type === 'webStream')
49
+ await pipeline(Readable.fromWeb(data), res);
45
50
  }
46
51
  catch (e) {
47
- // do nothing
52
+ // A streaming helper (pipeline/sendFileTrusted) may already have destroyed res on
53
+ // error (e.g. fs open EACCES mid-stream). Calling res.end() on a destroyed response
54
+ // never resolves, so skip it — otherwise the chain hangs forever.
55
+ if (res.destroyed) {
56
+ // nothing to flush; res is already torn down
57
+ }
58
+ else if (!res.headersSent) {
59
+ res.statusCode = e?.statusCode ?? 500;
60
+ await promisify(res.end.bind(res))();
61
+ }
62
+ else if (!res.writableEnded)
63
+ res.destroy(e);
48
64
  }
65
+ await awaitResFinished(res);
66
+ return;
49
67
  case undefined:
50
- // skip response. Some middleware may handle it outside the chain. For example, express middleware
68
+ // No setter was called. End the response with 404 instead of leaving it hung.
69
+ if (!res.headersSent)
70
+ res.statusCode = 404;
71
+ if (!res.writableEnded)
72
+ await promisify(res.end.bind(res))();
73
+ await awaitResFinished(res);
51
74
  return;
52
- case 'empty':
53
- bufferOrStream = Buffer.from('', charset);
54
- break;
55
75
  default:
56
- if (!res.getHeader('content-type'))
57
- res.setHeader('content-type', 'text/plain');
58
- throw new Error(`unsupported response type ${type}`);
76
+ // Unknown type: programming error. Surface it via console.error but still finish
77
+ // res so the invariant holds (chain resolves only after flush).
78
+ console.error(new Error(`unsupported response type ${type}`));
79
+ if (!res.headersSent)
80
+ res.statusCode = 500;
81
+ if (!res.writableEnded)
82
+ await promisify(res.end.bind(res))();
83
+ await awaitResFinished(res);
84
+ return;
59
85
  }
60
- if (res.headersSent) {
61
- // for example, chainStatic or setFile send the response directly
62
- if (res.writableFinished) {
63
- // skipped: response is already finished
86
+ // 204 No Content and 304 Not Modified must not carry a body or Content-Length.
87
+ if (res.statusCode !== 204 && res.statusCode !== 304) {
88
+ // Content-Length and ETag mirror what a GET would send, so a HEAD reports them too.
89
+ res.setHeader('content-length', buffer.length);
90
+ // no ETag for redirects: the empty body would share the empty-body tag with other empty
91
+ // responses, so a cached If-None-Match could wrongly 304 a redirect (dropping Location)
92
+ if (!disableEtag && type !== 'redirect') {
93
+ const etag = entityTag(buffer);
94
+ res.setHeader('ETag', etag);
95
+ if (isFreshETag(req, etag))
96
+ res.statusCode = 304;
64
97
  }
65
- else if (res.writableEnded) {
66
- // const defer = Promise.withResolvers()
67
- // res.addListener('finish', defer.resolve)
68
- // res.addListener('error', defer.reject)
69
- // await defer.promise
70
- // skipped: response is already ended
71
- // chunk is not fully flushed yet
72
- }
73
- else
74
- await promisify(res.end.bind(res))();
75
98
  }
76
- else {
99
+ // 204/304 carry no body or representation/framing metadata (ETag is a validator, so it stays).
100
+ // This catches both an explicitly-set 204/304 and the freshETag -> 304 transition above.
101
+ if (res.statusCode === 204 || res.statusCode === 304) {
77
102
  // https://github.com/expressjs/express/blob/980d881e3b023db079de60477a2588a91f046ca5/lib/response.js#L210
78
- // if (res.statusCode === 204) { // No Content
79
- // res.removeHeader('content-type')
80
- // res.removeHeader('content-length')
81
- // res.removeHeader('transfer-encoding')
82
- // // write nothing
83
- // }
84
- // if (res.statusCode === 205) { // reset content. Tell client to clear the form, etc.
85
- // res.setHeader('content-length', 0)
86
- // res.removeHeader('transfer-encoding')
87
- // } else
88
- if (req.method !== 'HEAD') {
89
- if (Buffer.isBuffer(bufferOrStream)) {
90
- // support: 304 (etag), zipping, file etag and last modified
91
- res.setHeader('content-length', bufferOrStream.length);
92
- if (!disableEtag) {
93
- const etag = entityTag(bufferOrStream);
94
- // const lastModified = res.getHeader('last-modified')
95
- res.setHeader('ETag', etag);
96
- if (isFreshETag(req, etag)) {
97
- res.removeHeader('content-type');
98
- res.removeHeader('content-length');
99
- res.removeHeader('transfer-encoding');
100
- res.statusCode = 304;
101
- // write nothing
102
- }
103
- else
104
- res.write(bufferOrStream);
105
- }
106
- else
107
- res.write(bufferOrStream);
108
- }
109
- else {
110
- bufferOrStream.pipe(res);
111
- return; // no res.end()
112
- }
113
- // we do not support content-encoding (gzip, deflate, br) and leave it to reverse proxy or CDN
114
- }
115
- await promisify(res.end.bind(res))();
103
+ res.removeHeader('content-type');
104
+ res.removeHeader('content-length');
105
+ res.removeHeader('transfer-encoding');
116
106
  }
107
+ else if (req.method !== 'HEAD')
108
+ res.write(buffer);
109
+ // we do not support content-encoding (gzip, deflate, br) and leave it to reverse proxy or CDN
110
+ await promisify(res.end.bind(res))();
111
+ await awaitResFinished(res);
112
+ function setContentType(contentType) {
113
+ if (res.headersSent || res.getHeader('content-type'))
114
+ return;
115
+ // only text/* carries a charset; binary (octet-stream) and JSON (always UTF-8 per RFC 8259) do not
116
+ const cs = charset ?? (contentType.startsWith('text/') ? 'utf-8' : undefined);
117
+ res.setHeader('content-type', `${contentType}${cs ? `; charset=${cs}` : ''}`);
118
+ }
119
+ }
120
+ // Resolves when res is fully flushed (finish) or the socket is gone (close).
121
+ // Used as the universal "we're done with this response" signal so every code path
122
+ // in writeRes can guarantee the chain doesn't unwind before bytes hit the wire.
123
+ function awaitResFinished(res) {
124
+ if (res.writableFinished || res.destroyed)
125
+ return Promise.resolve();
126
+ return new Promise(resolve => {
127
+ res.once('finish', done);
128
+ res.once('close', done);
129
+ function done() {
130
+ res.off('finish', done);
131
+ res.off('close', done);
132
+ resolve();
133
+ }
134
+ });
117
135
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dxHelpers.js","sourceRoot":"","sources":["../src/dxHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["import type {IncomingMessage, ServerResponse} from 'node:http'\nimport {Readable} from 'node:stream'\nimport type {ReadableStream as WebReadableStream} from 'node:stream/web'\nimport {pipeline} from 'node:stream/promises'\nimport {promisify} from 'node:util'\nimport {entityTag, isFreshETag} from './vendors/etag.js'\nimport {sendFileTrusted, type SendFileOptions, type HttpError} from './staticHelpers.js'\n\nexport type DxContext = {\n\tcharset?: BufferEncoding // not for redirect\n\tjsonBeautify?: boolean // json only\n\tdisableEtag?: boolean\n} & (\n\t| {\n\t\t\ttype: 'empty'\n\t\t\tdata: undefined\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'text'\n\t\t\tdata: string\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'html'\n\t\t\tdata: string\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'buffer'\n\t\t\tdata: Buffer\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'json'\n\t\t\tdata: any\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'redirect'\n\t\t\tdata: string\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'nodeStream'\n\t\t\tdata: Readable\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'webStream'\n\t\t\tdata: WebReadableStream\n\t\t\toptions: undefined\n\t }\n\t| {\n\t\t\ttype: 'file'\n\t\t\tdata: string\n\t\t\toptions?: SendFileOptions\n\t }\n)\n\nexport async function writeRes(\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\t{type, data, charset, jsonBeautify, disableEtag, options}: DxContext,\n) {\n\tif (res.headersSent) return\n\n\tlet buffer: Buffer | undefined\n\n\tswitch (type) {\n\t\tcase 'text':\n\t\tcase 'html':\n\t\t\tsetContentType(type === 'html' ? 'text/html' : 'text/plain')\n\t\t\tbuffer = Buffer.from(data ?? '', charset)\n\t\t\tbreak\n\t\tcase 'buffer':\n\t\t\tsetContentType('application/octet-stream')\n\t\t\tbuffer = data ?? Buffer.from('', charset)\n\t\t\tbreak\n\t\tcase 'json':\n\t\t\tsetContentType('application/json')\n\t\t\tbuffer =\n\t\t\t\tdata === undefined\n\t\t\t\t\t? Buffer.from('', charset)\n\t\t\t\t\t: Buffer.from(jsonBeautify ? JSON.stringify(data, null, 2) : JSON.stringify(data), charset)\n\t\t\tbreak\n\t\tcase 'redirect':\n\t\tcase 'empty':\n\t\t\tif (type === 'redirect') res.setHeader('location', data)\n\t\t\tbuffer = Buffer.from('', charset)\n\t\t\tbreak\n\t\t// Streaming paths own res.end() themselves (via pipeline/sendFileTrusted) and must be\n\t\t// awaited to fulfil the \"chain resolves after flush\" invariant.\n\t\tcase 'nodeStream':\n\t\tcase 'webStream':\n\t\t\tif (!data) {\n\t\t\t\tbuffer = Buffer.from('', charset)\n\t\t\t\tbreak\n\t\t\t}\n\t\tcase 'file':\n\t\t\tsetContentType('application/octet-stream')\n\t\t\ttry {\n\t\t\t\tif (type === 'file') await sendFileTrusted(req, res, data, options)\n\t\t\t\telse if (type === 'nodeStream') await pipeline(data, res)\n\t\t\t\telse if (type === 'webStream') await pipeline(Readable.fromWeb(data), res)\n\t\t\t} catch (e) {\n\t\t\t\t// A streaming helper (pipeline/sendFileTrusted) may already have destroyed res on\n\t\t\t\t// error (e.g. fs open EACCES mid-stream). Calling res.end() on a destroyed response\n\t\t\t\t// never resolves, so skip it — otherwise the chain hangs forever.\n\t\t\t\tif (res.destroyed) {\n\t\t\t\t\t// nothing to flush; res is already torn down\n\t\t\t\t} else if (!res.headersSent) {\n\t\t\t\t\tres.statusCode = (e as Partial<HttpError>)?.statusCode ?? 500\n\t\t\t\t\tawait promisify(res.end.bind(res))()\n\t\t\t\t} else if (!res.writableEnded) res.destroy(e as Error)\n\t\t\t\tconsole.error(e)\n\t\t\t}\n\t\t\tawait awaitResFinished(res)\n\t\t\treturn\n\t\tcase undefined:\n\t\t\t// No setter was called. End the response with 404 instead of leaving it hung.\n\t\t\tif (!res.headersSent) res.statusCode = 404\n\t\t\tif (!res.writableEnded) await promisify(res.end.bind(res))()\n\t\t\tawait awaitResFinished(res)\n\t\t\treturn\n\t\tdefault:\n\t\t\t// Unknown type: programming error. Surface it via console.error but still finish\n\t\t\t// res so the invariant holds (chain resolves only after flush).\n\t\t\tconsole.error(new Error(`unsupported response type ${type satisfies never}`))\n\t\t\tif (!res.headersSent) res.statusCode = 500\n\t\t\tif (!res.writableEnded) await promisify(res.end.bind(res))()\n\t\t\tawait awaitResFinished(res)\n\t\t\treturn\n\t}\n\n\t// https://github.com/expressjs/express/blob/980d881e3b023db079de60477a2588a91f046ca5/lib/response.js#L210\n\t// if (res.statusCode === 204) { // No Content\n\t// \tres.removeHeader('content-type')\n\t// \tres.removeHeader('content-length')\n\t// \tres.removeHeader('transfer-encoding')\n\t// \t// write nothing\n\t// }\n\t// if (res.statusCode === 205) { // reset content. Tell client to clear the form, etc.\n\t// \tres.setHeader('content-length', 0)\n\t// \tres.removeHeader('transfer-encoding')\n\t// } else\n\tif (req.method !== 'HEAD') {\n\t\tres.setHeader('content-length', buffer.length)\n\t\t// no ETag for redirects: the empty body would share the empty-body tag with other empty\n\t\t// responses, so a cached If-None-Match could wrongly 304 a redirect (dropping Location)\n\t\tif (!disableEtag && type !== 'redirect') {\n\t\t\tconst etag = entityTag(buffer)\n\t\t\tres.setHeader('ETag', etag)\n\t\t\tif (isFreshETag(req, etag)) {\n\t\t\t\tres.removeHeader('content-type')\n\t\t\t\tres.removeHeader('content-length')\n\t\t\t\tres.removeHeader('transfer-encoding')\n\t\t\t\tres.statusCode = 304\n\t\t\t} else res.write(buffer)\n\t\t} else res.write(buffer)\n\t\tawait promisify(res.end.bind(res))()\n\t}\n\t// we do not support content-encoding (gzip, deflate, br) and leave it to reverse proxy or CDN\n\n\tawait promisify(res.end.bind(res))()\n\n\tawait awaitResFinished(res)\n\n\tfunction setContentType(contentType: string) {\n\t\tif (res.headersSent || res.getHeader('content-type')) return\n\t\tres.setHeader('content-type', `${contentType}${charset ? `; charset=${charset}` : ''}`)\n\t}\n}\n\n// Resolves when res is fully flushed (finish) or the socket is gone (close).\n// Used as the universal \"we're done with this response\" signal so every code path\n// in writeRes can guarantee the chain doesn't unwind before bytes hit the wire.\nfunction awaitResFinished(res: ServerResponse) {\n\tif (res.writableFinished || res.destroyed) return Promise.resolve()\n\treturn new Promise<void>(resolve => {\n\t\tres.once('finish', done)\n\t\tres.once('close', done)\n\t\tfunction done() {\n\t\t\tres.off('finish', done)\n\t\t\tres.off('close', done)\n\t\t\tresolve()\n\t\t}\n\t})\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,gBAAgB,CAAA;AACvC,OAAO,EACN,2BAA2B,EAC3B,aAAa,EACb,WAAW,EACX,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,YAAY,GACZ,MAAM,kBAAkB,CAAA","sourcesContent":["export {writeRes} from './dxHelpers.js'\nexport {\n\tsetBufferBodyDefaultOptions,\n\tbufferFromReq,\n\tjsonFromReq,\n\trawFromReq,\n\ttextFromReq,\n\turlEncodedFromReq,\n\tqueryFromReq,\n} from './bodyHelpers.js'\n"]}
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { getReq, getRes, setHtml, setNodeStream, setWebStream, setJson, setBuffer, setRedirect, setText, setEmpty, setFile, makeDxContext, } from './dx.js';
2
2
  import { dxServer } from './dx.js';
3
- export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery, } from './body.js';
3
+ export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery } from './body.js';
4
4
  export { router } from './router.js';
5
5
  export { chainStatic } from './static.js';
6
6
  export { logJson, default as logger } from './logger.js';
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { getReq, getRes, setHtml, setNodeStream, setWebStream, setJson, setBuffer, setRedirect, setText, setEmpty, setFile, makeDxContext, } from './dx.js';
2
2
  import { dxServer } from './dx.js';
3
- export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery, } from './body.js';
3
+ export { getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery } from './body.js';
4
4
  export { router } from './router.js';
5
5
  export { chainStatic } from './static.js';
6
6
  export { logJson, default as logger } from './logger.js';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,MAAM,EACN,MAAM,EACN,OAAO,EACP,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACT,WAAW,EACX,OAAO,EACP,QAAQ,EACR,OAAO,EACP,aAAa,GACb,MAAM,SAAS,CAAA;AAChB,OAAO,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAA;AAChC,OAAO,EAAC,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAC,MAAM,WAAW,CAAA;AACtF,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAA;AAClC,OAAO,EAAC,WAAW,EAAC,MAAM,aAAa,CAAA;AACvC,OAAO,EAAC,OAAO,EAAE,OAAO,IAAI,MAAM,EAAC,MAAM,aAAa,CAAA;AAEtD,eAAe,QAAQ,CAAA","sourcesContent":["export {\n\tgetReq,\n\tgetRes,\n\tsetHtml,\n\tsetNodeStream,\n\tsetWebStream,\n\tsetJson,\n\tsetBuffer,\n\tsetRedirect,\n\tsetText,\n\tsetEmpty,\n\tsetFile,\n\tmakeDxContext,\n} from './dx.js'\nimport {dxServer} from './dx.js'\nexport {getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery} from './body.js'\nexport {router} from './router.js'\nexport {chainStatic} from './static.js'\nexport {logJson, default as logger} from './logger.js'\n\nexport default dxServer\n"]}
package/lib/logger.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export declare function logJson(json: any): void;
2
- declare const _default: (log?: typeof logJson) => (next: () => any) => any;
3
- export default _default;
2
+ export default function makeLogger(log?: typeof logJson, { timezoneOffset }?: {
3
+ timezoneOffset?: number | undefined;
4
+ }): (next: () => any) => any;
package/lib/logger.js CHANGED
@@ -4,53 +4,61 @@ export function logJson(json) {
4
4
  console.log(JSON.stringify(json));
5
5
  }
6
6
  let requestCount = 0;
7
- export default (log = logJson) => function logger(next) {
8
- const res = getRes();
9
- const req = getReq();
10
- const logId = requestCount++;
11
- const start = hrtime.bigint();
12
- const now = new Date(Date.now() + 9 * 60 * 60 * 1000); // jst
13
- log({
14
- level: 'info',
15
- id: logId,
16
- timestamp: [
17
- [
18
- now.getUTCFullYear(),
19
- String(now.getUTCMonth() + 1).padStart(2, '0'),
20
- String(now.getUTCDate()).padStart(2, '0'),
21
- ].join('-'),
22
- [
23
- String(now.getUTCHours()).padStart(2, '0'),
24
- String(now.getUTCMinutes()).padStart(2, '0'),
25
- [String(now.getUTCSeconds()).padStart(2, '0'), String(now.getUTCMilliseconds()).padStart(3, '0')].join('.'),
26
- ].join(':'),
27
- ].join('T'),
28
- remoteAddress: req.socket.remoteAddress,
29
- method: req.method,
30
- url: req.url,
31
- httpVersion: `HTTP/${req.httpVersion}`,
32
- headers: process.env.NODE_ENV === 'production'
33
- ? req.headers
34
- : Object.fromEntries(Object.entries(req.headers).filter(([k]) => [
35
- 'host',
36
- 'referer',
37
- 'referrer',
38
- 'user-agent',
39
- 'x-forwarded-proto',
40
- 'x-forwarded-host',
41
- 'x-forwarded-for',
42
- ].includes(k))),
43
- });
44
- res.once('finish', end).once('close', end).once('error', end);
45
- return next();
46
- function end() {
47
- res.off('finish', end).off('close', end).off('error', end);
48
- const durationNs = hrtime.bigint() - start;
7
+ export default function makeLogger(log = logJson, { timezoneOffset = 0 } = {}) {
8
+ // offset designator so a shifted timestamp is unambiguous (e.g. +09:00, -05:00, +00:00)
9
+ const offsetMin = Math.round(timezoneOffset * 60);
10
+ const offset = `${offsetMin < 0 ? '-' : '+'}${[
11
+ String(Math.floor(Math.abs(offsetMin) / 60)).padStart(2, '0'),
12
+ String(Math.abs(offsetMin) % 60).padStart(2, '0'),
13
+ ].join(':')}`;
14
+ return function logger(next) {
15
+ const res = getRes();
16
+ const req = getReq();
17
+ const logId = requestCount++;
18
+ const start = hrtime.bigint();
19
+ const now = new Date(Date.now() + timezoneOffset * 60 * 60 * 1000);
49
20
  log({
50
21
  level: 'info',
51
22
  id: logId,
52
- duration: Number(durationNs) / 1e6, // ms
53
- headers: res.getHeaders(),
23
+ timestamp: [
24
+ [
25
+ now.getUTCFullYear(),
26
+ String(now.getUTCMonth() + 1).padStart(2, '0'),
27
+ String(now.getUTCDate()).padStart(2, '0'),
28
+ ].join('-'),
29
+ [
30
+ String(now.getUTCHours()).padStart(2, '0'),
31
+ String(now.getUTCMinutes()).padStart(2, '0'),
32
+ [String(now.getUTCSeconds()).padStart(2, '0'), String(now.getUTCMilliseconds()).padStart(3, '0')].join('.'),
33
+ ].join(':'),
34
+ ].join('T') + offset,
35
+ remoteAddress: req.socket.remoteAddress,
36
+ method: req.method,
37
+ url: req.url,
38
+ httpVersion: `HTTP/${req.httpVersion}`,
39
+ headers: process.env.NODE_ENV === 'production'
40
+ ? req.headers
41
+ : Object.fromEntries(Object.entries(req.headers).filter(([k]) => [
42
+ 'host',
43
+ 'referer',
44
+ 'referrer',
45
+ 'user-agent',
46
+ 'x-forwarded-proto',
47
+ 'x-forwarded-host',
48
+ 'x-forwarded-for',
49
+ ].includes(k))),
54
50
  });
55
- }
56
- };
51
+ res.once('finish', end).once('close', end).once('error', end);
52
+ return next();
53
+ function end() {
54
+ res.off('finish', end).off('close', end).off('error', end);
55
+ const durationNs = hrtime.bigint() - start;
56
+ log({
57
+ level: 'info',
58
+ id: logId,
59
+ duration: Number(durationNs) / 1e6, // ms
60
+ headers: res.getHeaders(),
61
+ });
62
+ }
63
+ };
64
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAE,MAAM,EAAC,MAAM,SAAS,CAAA;AACtC,OAAO,EAAC,MAAM,EAAC,MAAM,cAAc,CAAA;AAEnC,MAAM,UAAU,OAAO,CAAC,IAAS;IAChC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;AAClC,CAAC;AAED,IAAI,YAAY,GAAG,CAAC,CAAA;AACpB,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,GAAG,GAAG,OAAO,EAAE,EAAC,cAAc,GAAG,CAAC,EAAC,GAAG,EAAE;IAC1E,OAAO,SAAS,MAAM,CAAC,IAAe;QACrC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;QACpB,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;QACpB,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;QAE5B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;QAC7B,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;QAElE,GAAG,CAAC;YACH,KAAK,EAAE,MAAM;YACb,EAAE,EAAE,KAAK;YACT,SAAS,EAAE;gBACV;oBACC,GAAG,CAAC,cAAc,EAAE;oBACpB,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;oBAC9C,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;iBACzC,CAAC,IAAI,CAAC,GAAG,CAAC;gBACX;oBACC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;oBAC1C,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;oBAC5C,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;iBAC3G,CAAC,IAAI,CAAC,GAAG,CAAC;aACX,CAAC,IAAI,CAAC,GAAG,CAAC;YACX,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,aAAa;YACvC,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,WAAW,EAAE,QAAQ,GAAG,CAAC,WAAW,EAAE;YACtC,OAAO,EACN,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;gBACpC,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,MAAM,CAAC,WAAW,CAClB,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAC1C;oBACC,MAAM;oBACN,SAAS;oBACT,UAAU;oBACV,YAAY;oBACZ,mBAAmB;oBACnB,kBAAkB;oBAClB,iBAAiB;iBACjB,CAAC,QAAQ,CAAC,CAAC,CAAC,CACb,CACD;SACJ,CAAC,CAAA;QAEF,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;QAE7D,OAAO,IAAI,EAAE,CAAA;QAEb,SAAS,GAAG;YACX,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;YAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,KAAK,CAAA;YAC1C,GAAG,CAAC;gBACH,KAAK,EAAE,MAAM;gBACb,EAAE,EAAE,KAAK;gBACT,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,GAAG,EAAE,KAAK;gBACzC,OAAO,EAAE,GAAG,CAAC,UAAU,EAAE;aACzB,CAAC,CAAA;QACH,CAAC;IACF,CAAC,CAAA;AACF,CAAC","sourcesContent":["import {getReq, getRes} from './dx.js'\nimport {hrtime} from 'node:process'\n\nexport function logJson(json: any) {\n\tconsole.log(JSON.stringify(json))\n}\n\nlet requestCount = 0\nexport default function makeLogger(log = logJson, {timezoneOffset = 0} = {}) {\n\treturn function logger(next: () => any) {\n\t\tconst res = getRes()\n\t\tconst req = getReq()\n\t\tconst logId = requestCount++\n\n\t\tconst start = hrtime.bigint()\n\t\tconst now = new Date(Date.now() + timezoneOffset * 60 * 60 * 1000)\n\n\t\tlog({\n\t\t\tlevel: 'info',\n\t\t\tid: logId,\n\t\t\ttimestamp: [\n\t\t\t\t[\n\t\t\t\t\tnow.getUTCFullYear(),\n\t\t\t\t\tString(now.getUTCMonth() + 1).padStart(2, '0'),\n\t\t\t\t\tString(now.getUTCDate()).padStart(2, '0'),\n\t\t\t\t].join('-'),\n\t\t\t\t[\n\t\t\t\t\tString(now.getUTCHours()).padStart(2, '0'),\n\t\t\t\t\tString(now.getUTCMinutes()).padStart(2, '0'),\n\t\t\t\t\t[String(now.getUTCSeconds()).padStart(2, '0'), String(now.getUTCMilliseconds()).padStart(3, '0')].join('.'),\n\t\t\t\t].join(':'),\n\t\t\t].join('T'),\n\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\tmethod: req.method,\n\t\t\turl: req.url,\n\t\t\thttpVersion: `HTTP/${req.httpVersion}`,\n\t\t\theaders:\n\t\t\t\tprocess.env.NODE_ENV === 'production'\n\t\t\t\t\t? req.headers\n\t\t\t\t\t: Object.fromEntries(\n\t\t\t\t\t\t\tObject.entries(req.headers).filter(([k]) =>\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t'host',\n\t\t\t\t\t\t\t\t\t'referer',\n\t\t\t\t\t\t\t\t\t'referrer',\n\t\t\t\t\t\t\t\t\t'user-agent',\n\t\t\t\t\t\t\t\t\t'x-forwarded-proto',\n\t\t\t\t\t\t\t\t\t'x-forwarded-host',\n\t\t\t\t\t\t\t\t\t'x-forwarded-for',\n\t\t\t\t\t\t\t\t].includes(k),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t})\n\n\t\tres.once('finish', end).once('close', end).once('error', end)\n\n\t\treturn next()\n\n\t\tfunction end() {\n\t\t\tres.off('finish', end).off('close', end).off('error', end)\n\t\t\tconst durationNs = hrtime.bigint() - start\n\t\t\tlog({\n\t\t\t\tlevel: 'info',\n\t\t\t\tid: logId,\n\t\t\t\tduration: Number(durationNs) / 1e6, // ms\n\t\t\t\theaders: res.getHeaders(),\n\t\t\t})\n\t\t}\n\t}\n}\n"]}
package/lib/router.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import { getReq } from './dx.js';
2
2
  import { urlFromReq } from './bodyHelpers.js';
3
3
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
4
- const allMethods = [
5
- 'get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch'
6
- ];
4
+ const allMethods = ['get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch'];
7
5
  function makeRouter(method, // undefined means any method
8
6
  routes, { prefix = '' } = {}) {
9
7
  const routeWithUrlPatterns = routes.map(([pattern, handler]) => [new URLPattern({ pathname: `${prefix}${pattern}` }), handler]);
10
8
  return next => {
11
9
  const req = getReq();
12
- if (method !== undefined && req.method !== method.toUpperCase())
10
+ const want = method?.toUpperCase();
11
+ // a GET route also answers HEAD: HEAD must behave like GET, and writeRes strips the body
12
+ if (want !== undefined && req.method !== want && (want !== 'GET' || req.method !== 'HEAD'))
13
13
  return next();
14
14
  for (const [urlPattern, handler] of routeWithUrlPatterns) {
15
15
  // '' matches nothing
@@ -37,7 +37,7 @@ export const router = {
37
37
  return typeof params[0] === 'string'
38
38
  ? makeRouter(undefined, [[params[0], params[1]]], params[2])
39
39
  : makeRouter(undefined, Object.entries(params[0]), params[1]);
40
- }
40
+ },
41
41
  };
42
42
  for (const method of allMethods)
43
43
  router[method] = router.method.bind(router, method);