@naturalcycles/backend-lib 9.77.2 → 9.78.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.
- package/dist/compressible.d.ts +9 -0
- package/dist/compressible.js +57 -0
- package/dist/onFinished.d.ts +14 -2
- package/dist/onFinished.js +35 -2
- package/dist/onHeaders.d.ts +11 -0
- package/dist/onHeaders.js +56 -0
- package/dist/server/compressionMiddleware.js +3 -3
- package/dist/vary.d.ts +8 -0
- package/dist/vary.js +25 -0
- package/package.json +2 -10
- package/src/compressible.ts +59 -0
- package/src/onFinished.ts +43 -2
- package/src/onHeaders.ts +66 -0
- package/src/server/compressionMiddleware.ts +3 -3
- package/src/vary.ts +29 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the given Content-Type (parameters, e.g `; charset=utf-8`, are ignored) is compressible.
|
|
3
|
+
*
|
|
4
|
+
* Vendored (and modernized) from the `compressible` package.
|
|
5
|
+
* Instead of loading the whole `mime-db` (~2500 types), it's reduced to the equivalent:
|
|
6
|
+
* a regex + the list of compressible types that the regex doesn't match (as of mime-db@1.54.0).
|
|
7
|
+
* No type matched by the regex is marked as non-compressible in mime-db.
|
|
8
|
+
*/
|
|
9
|
+
export declare function compressible(type: string): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the given Content-Type (parameters, e.g `; charset=utf-8`, are ignored) is compressible.
|
|
3
|
+
*
|
|
4
|
+
* Vendored (and modernized) from the `compressible` package.
|
|
5
|
+
* Instead of loading the whole `mime-db` (~2500 types), it's reduced to the equivalent:
|
|
6
|
+
* a regex + the list of compressible types that the regex doesn't match (as of mime-db@1.54.0).
|
|
7
|
+
* No type matched by the regex is marked as non-compressible in mime-db.
|
|
8
|
+
*/
|
|
9
|
+
export function compressible(type) {
|
|
10
|
+
const mime = EXTRACT_TYPE_REGEX.exec(type)[1].toLowerCase();
|
|
11
|
+
return COMPRESSIBLE_TYPE_REGEX.test(mime) || COMPRESSIBLE_TYPES.has(mime);
|
|
12
|
+
}
|
|
13
|
+
const EXTRACT_TYPE_REGEX = /^\s*([^;\s]*)/;
|
|
14
|
+
const COMPRESSIBLE_TYPE_REGEX = /^text\/|\+(?:json|text|xml)$/;
|
|
15
|
+
const COMPRESSIBLE_TYPES = new Set([
|
|
16
|
+
'application/dart',
|
|
17
|
+
'application/ecmascript',
|
|
18
|
+
'application/javascript',
|
|
19
|
+
'application/json',
|
|
20
|
+
'application/octet-stream',
|
|
21
|
+
'application/postscript',
|
|
22
|
+
'application/raml+yaml',
|
|
23
|
+
'application/rtf',
|
|
24
|
+
'application/tar',
|
|
25
|
+
'application/toml',
|
|
26
|
+
'application/vnd.dart',
|
|
27
|
+
'application/vnd.ms-fontobject',
|
|
28
|
+
'application/vnd.ms-opentype',
|
|
29
|
+
'application/wasm',
|
|
30
|
+
'application/x-httpd-php',
|
|
31
|
+
'application/x-javascript',
|
|
32
|
+
'application/x-ns-proxy-autoconfig',
|
|
33
|
+
'application/x-sh',
|
|
34
|
+
'application/x-tar',
|
|
35
|
+
'application/x-virtualbox-hdd',
|
|
36
|
+
'application/x-virtualbox-ova',
|
|
37
|
+
'application/x-virtualbox-ovf',
|
|
38
|
+
'application/x-virtualbox-vbox',
|
|
39
|
+
'application/x-virtualbox-vdi',
|
|
40
|
+
'application/x-virtualbox-vhd',
|
|
41
|
+
'application/x-virtualbox-vmdk',
|
|
42
|
+
'application/x-www-form-urlencoded',
|
|
43
|
+
'application/xml',
|
|
44
|
+
'application/xml-dtd',
|
|
45
|
+
'font/otf',
|
|
46
|
+
'font/ttf',
|
|
47
|
+
'image/bmp',
|
|
48
|
+
'image/vnd.adobe.photoshop',
|
|
49
|
+
'image/vnd.microsoft.icon',
|
|
50
|
+
'image/vnd.ms-dds',
|
|
51
|
+
'image/x-icon',
|
|
52
|
+
'image/x-ms-bmp',
|
|
53
|
+
'message/rfc822',
|
|
54
|
+
'model/gltf-binary',
|
|
55
|
+
'x-shader/x-fragment',
|
|
56
|
+
'x-shader/x-vertex',
|
|
57
|
+
]);
|
package/dist/onFinished.d.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Invokes the listener once the request/response has finished (completed, closed or errored).
|
|
4
|
+
* If it has already finished - the listener is invoked asynchronously (on the next tick).
|
|
5
|
+
*
|
|
6
|
+
* Listener runs in the async context (e.g AsyncLocalStorage) of the onFinished caller.
|
|
7
|
+
*
|
|
8
|
+
* Vendored (and modernized) from the `on-finished` package, built on top of `stream.finished`.
|
|
9
|
+
* Stream listeners are attached once per msg (subsequent calls are queued), as `stream.finished`
|
|
10
|
+
* is too expensive to call per listener (and would exceed the default MaxListeners).
|
|
11
|
+
*/
|
|
12
|
+
export declare function onFinished<T extends IncomingMessage | ServerResponse>(msg: T, listener: Listener<T>): T;
|
|
13
|
+
type Listener<T> = (err: Error | null, msg: T) => void;
|
|
14
|
+
export {};
|
package/dist/onFinished.js
CHANGED
|
@@ -1,2 +1,35 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
2
|
+
import { finished } from 'node:stream';
|
|
3
|
+
/**
|
|
4
|
+
* Invokes the listener once the request/response has finished (completed, closed or errored).
|
|
5
|
+
* If it has already finished - the listener is invoked asynchronously (on the next tick).
|
|
6
|
+
*
|
|
7
|
+
* Listener runs in the async context (e.g AsyncLocalStorage) of the onFinished caller.
|
|
8
|
+
*
|
|
9
|
+
* Vendored (and modernized) from the `on-finished` package, built on top of `stream.finished`.
|
|
10
|
+
* Stream listeners are attached once per msg (subsequent calls are queued), as `stream.finished`
|
|
11
|
+
* is too expensive to call per listener (and would exceed the default MaxListeners).
|
|
12
|
+
*/
|
|
13
|
+
export function onFinished(msg, listener) {
|
|
14
|
+
const m = msg;
|
|
15
|
+
const queue = m[kQueue];
|
|
16
|
+
if (queue) {
|
|
17
|
+
queue.push(bindAsyncContext(listener));
|
|
18
|
+
return msg;
|
|
19
|
+
}
|
|
20
|
+
const q = (m[kQueue] = [bindAsyncContext(listener)]);
|
|
21
|
+
finished(msg, err => {
|
|
22
|
+
m[kQueue] = undefined;
|
|
23
|
+
for (const fn of q)
|
|
24
|
+
fn(err ?? null, msg);
|
|
25
|
+
});
|
|
26
|
+
return msg;
|
|
27
|
+
}
|
|
28
|
+
const kQueue = Symbol('onFinished');
|
|
29
|
+
/**
|
|
30
|
+
* Faster alternative to `AsyncResource.bind` (which is ~0.8µs per call).
|
|
31
|
+
*/
|
|
32
|
+
function bindAsyncContext(fn) {
|
|
33
|
+
const resource = new AsyncResource('onFinished');
|
|
34
|
+
return (err, msg) => resource.runInAsyncScope(fn, null, err, msg);
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Invokes the listener once, right before the response writes its headers
|
|
4
|
+
* (via `res.writeHead`, which is also called implicitly by the first `write`/`end`).
|
|
5
|
+
*
|
|
6
|
+
* Headers passed to `writeHead` are applied to `res` before the listener runs,
|
|
7
|
+
* so the listener sees (and can modify) the final headers and statusCode.
|
|
8
|
+
*
|
|
9
|
+
* Vendored (and modernized) from the `on-headers` package.
|
|
10
|
+
*/
|
|
11
|
+
export declare function onHeaders(res: ServerResponse, listener: (this: ServerResponse) => void): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invokes the listener once, right before the response writes its headers
|
|
3
|
+
* (via `res.writeHead`, which is also called implicitly by the first `write`/`end`).
|
|
4
|
+
*
|
|
5
|
+
* Headers passed to `writeHead` are applied to `res` before the listener runs,
|
|
6
|
+
* so the listener sees (and can modify) the final headers and statusCode.
|
|
7
|
+
*
|
|
8
|
+
* Vendored (and modernized) from the `on-headers` package.
|
|
9
|
+
*/
|
|
10
|
+
export function onHeaders(res, listener) {
|
|
11
|
+
// Loosely typed, to call it with the (statusCode, reason?) signature, bypassing overloads
|
|
12
|
+
const prevWriteHead = res.writeHead;
|
|
13
|
+
let fired = false;
|
|
14
|
+
res.writeHead = function writeHead(statusCode, reasonOrHeaders, headers) {
|
|
15
|
+
if (fired)
|
|
16
|
+
return prevWriteHead.apply(this, arguments);
|
|
17
|
+
fired = true;
|
|
18
|
+
const reason = typeof reasonOrHeaders === 'string' ? reasonOrHeaders : undefined;
|
|
19
|
+
headers = reason === undefined ? reasonOrHeaders : headers;
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
if (headers)
|
|
22
|
+
setHeaders(this, headers);
|
|
23
|
+
listener.call(this);
|
|
24
|
+
// Reason phrase is dropped if the listener changed the statusCode
|
|
25
|
+
return reason !== undefined && this.statusCode === statusCode
|
|
26
|
+
? prevWriteHead.call(this, statusCode, reason)
|
|
27
|
+
: prevWriteHead.call(this, this.statusCode);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function setHeaders(res, headers) {
|
|
31
|
+
if (!Array.isArray(headers)) {
|
|
32
|
+
for (const k of Object.keys(headers)) {
|
|
33
|
+
if (k)
|
|
34
|
+
res.setHeader(k, headers[k]);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(headers[0])) {
|
|
39
|
+
// 2D: [[k, v], [k, v]]
|
|
40
|
+
for (const [k, v] of headers) {
|
|
41
|
+
if (k)
|
|
42
|
+
res.setHeader(k, v);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// 1D (raw): [k, v, k, v], keys may repeat
|
|
47
|
+
if (headers.length % 2)
|
|
48
|
+
throw new TypeError('headers array is malformed');
|
|
49
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
50
|
+
res.removeHeader(headers[i]);
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
53
|
+
if (headers[i])
|
|
54
|
+
res.appendHeader(headers[i], headers[i + 1]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* MIT Licensed
|
|
11
11
|
*/
|
|
12
12
|
import zlib from 'node:zlib';
|
|
13
|
-
import compressible from 'compressible';
|
|
14
13
|
// @ts-expect-error no types
|
|
15
14
|
import Negotiator from 'negotiator';
|
|
16
|
-
import
|
|
17
|
-
import
|
|
15
|
+
import { compressible } from '../compressible.js';
|
|
16
|
+
import { onHeaders } from '../onHeaders.js';
|
|
17
|
+
import { vary } from '../vary.js';
|
|
18
18
|
const SUPPORTED_ENCODING = ['zstd', 'br', 'gzip', 'deflate', 'identity'];
|
|
19
19
|
const PREFERRED_ENCODING = ['zstd', 'br', 'gzip'];
|
|
20
20
|
const encodingSupported = new Set(['gzip', 'deflate', 'identity', 'br', 'zstd']);
|
package/dist/vary.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Adds the field (header name) to the response's Vary header, unless it's already there
|
|
4
|
+
* (case-insensitive). Vary: * stays as is, and adding '*' replaces the whole header with '*'.
|
|
5
|
+
*
|
|
6
|
+
* Vendored (and modernized) from the `vary` package.
|
|
7
|
+
*/
|
|
8
|
+
export declare function vary(res: ServerResponse, field: string): void;
|
package/dist/vary.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds the field (header name) to the response's Vary header, unless it's already there
|
|
3
|
+
* (case-insensitive). Vary: * stays as is, and adding '*' replaces the whole header with '*'.
|
|
4
|
+
*
|
|
5
|
+
* Vendored (and modernized) from the `vary` package.
|
|
6
|
+
*/
|
|
7
|
+
export function vary(res, field) {
|
|
8
|
+
const header = res.getHeader('Vary');
|
|
9
|
+
const value = Array.isArray(header) ? header.join(', ') : String(header ?? '');
|
|
10
|
+
if (!value) {
|
|
11
|
+
res.setHeader('Vary', field);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const fields = new Set(value
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.split(',')
|
|
17
|
+
.map(f => f.trim()));
|
|
18
|
+
if (field === '*' || fields.has('*')) {
|
|
19
|
+
if (value !== '*')
|
|
20
|
+
res.setHeader('Vary', '*');
|
|
21
|
+
}
|
|
22
|
+
else if (!fields.has(field.toLowerCase())) {
|
|
23
|
+
res.setHeader('Vary', `${value}, ${field}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/backend-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.78.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/db-lib": "^10",
|
|
7
7
|
"@naturalcycles/js-lib": "^15",
|
|
@@ -10,21 +10,13 @@
|
|
|
10
10
|
"@types/cookie-parser": "^1",
|
|
11
11
|
"@types/cors": "^2",
|
|
12
12
|
"@types/express": "^5",
|
|
13
|
-
"@types/compressible": "^2",
|
|
14
|
-
"@types/on-finished": "^2",
|
|
15
|
-
"@types/on-headers": "^1",
|
|
16
|
-
"@types/vary": "^1",
|
|
17
13
|
"busboy": "^1",
|
|
18
|
-
"compressible": "^2",
|
|
19
14
|
"cookie-parser": "^1",
|
|
20
15
|
"cors": "^2",
|
|
21
16
|
"express": "^5",
|
|
22
17
|
"helmet": "^8",
|
|
23
18
|
"negotiator": "^1",
|
|
24
|
-
"
|
|
25
|
-
"on-finished": "^2",
|
|
26
|
-
"tslib": "^2",
|
|
27
|
-
"vary": "^1"
|
|
19
|
+
"tslib": "^2"
|
|
28
20
|
},
|
|
29
21
|
"peerDependencies": {
|
|
30
22
|
"@sentry/node-core": "^10",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the given Content-Type (parameters, e.g `; charset=utf-8`, are ignored) is compressible.
|
|
3
|
+
*
|
|
4
|
+
* Vendored (and modernized) from the `compressible` package.
|
|
5
|
+
* Instead of loading the whole `mime-db` (~2500 types), it's reduced to the equivalent:
|
|
6
|
+
* a regex + the list of compressible types that the regex doesn't match (as of mime-db@1.54.0).
|
|
7
|
+
* No type matched by the regex is marked as non-compressible in mime-db.
|
|
8
|
+
*/
|
|
9
|
+
export function compressible(type: string): boolean {
|
|
10
|
+
const mime = EXTRACT_TYPE_REGEX.exec(type)![1]!.toLowerCase()
|
|
11
|
+
return COMPRESSIBLE_TYPE_REGEX.test(mime) || COMPRESSIBLE_TYPES.has(mime)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EXTRACT_TYPE_REGEX = /^\s*([^;\s]*)/
|
|
15
|
+
const COMPRESSIBLE_TYPE_REGEX = /^text\/|\+(?:json|text|xml)$/
|
|
16
|
+
|
|
17
|
+
const COMPRESSIBLE_TYPES = new Set([
|
|
18
|
+
'application/dart',
|
|
19
|
+
'application/ecmascript',
|
|
20
|
+
'application/javascript',
|
|
21
|
+
'application/json',
|
|
22
|
+
'application/octet-stream',
|
|
23
|
+
'application/postscript',
|
|
24
|
+
'application/raml+yaml',
|
|
25
|
+
'application/rtf',
|
|
26
|
+
'application/tar',
|
|
27
|
+
'application/toml',
|
|
28
|
+
'application/vnd.dart',
|
|
29
|
+
'application/vnd.ms-fontobject',
|
|
30
|
+
'application/vnd.ms-opentype',
|
|
31
|
+
'application/wasm',
|
|
32
|
+
'application/x-httpd-php',
|
|
33
|
+
'application/x-javascript',
|
|
34
|
+
'application/x-ns-proxy-autoconfig',
|
|
35
|
+
'application/x-sh',
|
|
36
|
+
'application/x-tar',
|
|
37
|
+
'application/x-virtualbox-hdd',
|
|
38
|
+
'application/x-virtualbox-ova',
|
|
39
|
+
'application/x-virtualbox-ovf',
|
|
40
|
+
'application/x-virtualbox-vbox',
|
|
41
|
+
'application/x-virtualbox-vdi',
|
|
42
|
+
'application/x-virtualbox-vhd',
|
|
43
|
+
'application/x-virtualbox-vmdk',
|
|
44
|
+
'application/x-www-form-urlencoded',
|
|
45
|
+
'application/xml',
|
|
46
|
+
'application/xml-dtd',
|
|
47
|
+
'font/otf',
|
|
48
|
+
'font/ttf',
|
|
49
|
+
'image/bmp',
|
|
50
|
+
'image/vnd.adobe.photoshop',
|
|
51
|
+
'image/vnd.microsoft.icon',
|
|
52
|
+
'image/vnd.ms-dds',
|
|
53
|
+
'image/x-icon',
|
|
54
|
+
'image/x-ms-bmp',
|
|
55
|
+
'message/rfc822',
|
|
56
|
+
'model/gltf-binary',
|
|
57
|
+
'x-shader/x-fragment',
|
|
58
|
+
'x-shader/x-vertex',
|
|
59
|
+
])
|
package/src/onFinished.ts
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AsyncResource } from 'node:async_hooks'
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
|
+
import { finished } from 'node:stream'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Invokes the listener once the request/response has finished (completed, closed or errored).
|
|
7
|
+
* If it has already finished - the listener is invoked asynchronously (on the next tick).
|
|
8
|
+
*
|
|
9
|
+
* Listener runs in the async context (e.g AsyncLocalStorage) of the onFinished caller.
|
|
10
|
+
*
|
|
11
|
+
* Vendored (and modernized) from the `on-finished` package, built on top of `stream.finished`.
|
|
12
|
+
* Stream listeners are attached once per msg (subsequent calls are queued), as `stream.finished`
|
|
13
|
+
* is too expensive to call per listener (and would exceed the default MaxListeners).
|
|
14
|
+
*/
|
|
15
|
+
export function onFinished<T extends IncomingMessage | ServerResponse>(
|
|
16
|
+
msg: T,
|
|
17
|
+
listener: Listener<T>,
|
|
18
|
+
): T {
|
|
19
|
+
const m = msg as T & { [kQueue]?: Listener<T>[] }
|
|
20
|
+
const queue = m[kQueue]
|
|
21
|
+
if (queue) {
|
|
22
|
+
queue.push(bindAsyncContext(listener))
|
|
23
|
+
return msg
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const q = (m[kQueue] = [bindAsyncContext(listener)])
|
|
27
|
+
finished(msg, err => {
|
|
28
|
+
m[kQueue] = undefined
|
|
29
|
+
for (const fn of q) fn(err ?? null, msg)
|
|
30
|
+
})
|
|
31
|
+
return msg
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Listener<T> = (err: Error | null, msg: T) => void
|
|
35
|
+
|
|
36
|
+
const kQueue = Symbol('onFinished')
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Faster alternative to `AsyncResource.bind` (which is ~0.8µs per call).
|
|
40
|
+
*/
|
|
41
|
+
function bindAsyncContext<T>(fn: Listener<T>): Listener<T> {
|
|
42
|
+
const resource = new AsyncResource('onFinished')
|
|
43
|
+
return (err, msg) => resource.runInAsyncScope(fn, null, err, msg)
|
|
44
|
+
}
|
package/src/onHeaders.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from 'node:http'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Invokes the listener once, right before the response writes its headers
|
|
5
|
+
* (via `res.writeHead`, which is also called implicitly by the first `write`/`end`).
|
|
6
|
+
*
|
|
7
|
+
* Headers passed to `writeHead` are applied to `res` before the listener runs,
|
|
8
|
+
* so the listener sees (and can modify) the final headers and statusCode.
|
|
9
|
+
*
|
|
10
|
+
* Vendored (and modernized) from the `on-headers` package.
|
|
11
|
+
*/
|
|
12
|
+
export function onHeaders(res: ServerResponse, listener: (this: ServerResponse) => void): void {
|
|
13
|
+
// Loosely typed, to call it with the (statusCode, reason?) signature, bypassing overloads
|
|
14
|
+
const prevWriteHead = res.writeHead as (statusCode: number, reason?: string) => ServerResponse
|
|
15
|
+
let fired = false
|
|
16
|
+
|
|
17
|
+
res.writeHead = function writeHead(
|
|
18
|
+
this: ServerResponse,
|
|
19
|
+
statusCode: number,
|
|
20
|
+
reasonOrHeaders?: string | Headers,
|
|
21
|
+
headers?: Headers,
|
|
22
|
+
) {
|
|
23
|
+
if (fired) return prevWriteHead.apply(this, arguments as any)
|
|
24
|
+
fired = true
|
|
25
|
+
|
|
26
|
+
const reason = typeof reasonOrHeaders === 'string' ? reasonOrHeaders : undefined
|
|
27
|
+
headers = reason === undefined ? (reasonOrHeaders as Headers | undefined) : headers
|
|
28
|
+
|
|
29
|
+
this.statusCode = statusCode
|
|
30
|
+
if (headers) setHeaders(this, headers)
|
|
31
|
+
listener.call(this)
|
|
32
|
+
|
|
33
|
+
// Reason phrase is dropped if the listener changed the statusCode
|
|
34
|
+
return reason !== undefined && this.statusCode === statusCode
|
|
35
|
+
? prevWriteHead.call(this, statusCode, reason)
|
|
36
|
+
: prevWriteHead.call(this, this.statusCode)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Headers = OutgoingHttpHeaders | OutgoingHttpHeader[]
|
|
41
|
+
|
|
42
|
+
function setHeaders(res: ServerResponse, headers: Headers): void {
|
|
43
|
+
if (!Array.isArray(headers)) {
|
|
44
|
+
for (const k of Object.keys(headers)) {
|
|
45
|
+
if (k) res.setHeader(k, headers[k]!)
|
|
46
|
+
}
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(headers[0])) {
|
|
51
|
+
// 2D: [[k, v], [k, v]]
|
|
52
|
+
for (const [k, v] of headers as unknown as [string, OutgoingHttpHeader][]) {
|
|
53
|
+
if (k) res.setHeader(k, v)
|
|
54
|
+
}
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 1D (raw): [k, v, k, v], keys may repeat
|
|
59
|
+
if (headers.length % 2) throw new TypeError('headers array is malformed')
|
|
60
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
61
|
+
res.removeHeader(headers[i] as string)
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
64
|
+
if (headers[i]) res.appendHeader(headers[i] as string, headers[i + 1] as string | string[])
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import zlib from 'node:zlib'
|
|
14
|
-
import compressible from 'compressible'
|
|
15
14
|
// @ts-expect-error no types
|
|
16
15
|
import Negotiator from 'negotiator'
|
|
17
|
-
import
|
|
18
|
-
import
|
|
16
|
+
import { compressible } from '../compressible.js'
|
|
17
|
+
import { onHeaders } from '../onHeaders.js'
|
|
18
|
+
import { vary } from '../vary.js'
|
|
19
19
|
import type { BackendRequest, BackendRequestHandler, BackendResponse } from './server.model.js'
|
|
20
20
|
|
|
21
21
|
export interface CompressionOptions {
|
package/src/vary.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adds the field (header name) to the response's Vary header, unless it's already there
|
|
5
|
+
* (case-insensitive). Vary: * stays as is, and adding '*' replaces the whole header with '*'.
|
|
6
|
+
*
|
|
7
|
+
* Vendored (and modernized) from the `vary` package.
|
|
8
|
+
*/
|
|
9
|
+
export function vary(res: ServerResponse, field: string): void {
|
|
10
|
+
const header = res.getHeader('Vary')
|
|
11
|
+
const value = Array.isArray(header) ? header.join(', ') : String(header ?? '')
|
|
12
|
+
if (!value) {
|
|
13
|
+
res.setHeader('Vary', field)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fields = new Set(
|
|
18
|
+
value
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.split(',')
|
|
21
|
+
.map(f => f.trim()),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if (field === '*' || fields.has('*')) {
|
|
25
|
+
if (value !== '*') res.setHeader('Vary', '*')
|
|
26
|
+
} else if (!fields.has(field.toLowerCase())) {
|
|
27
|
+
res.setHeader('Vary', `${value}, ${field}`)
|
|
28
|
+
}
|
|
29
|
+
}
|