-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathContentLengthMiddleware.js
More file actions
114 lines (107 loc) · 4.12 KB
/
ContentLengthMiddleware.js
File metadata and controls
114 lines (107 loc) · 4.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
/** @typedef {import('../data/custom-types.js').MiddlewareFunction} MiddlewareFunction */
/** @typedef {import('../data/custom-types.js').ResponseFinalizer} ResponseFinalizer */
import { Transform } from 'node:stream';
/**
* @typedef {Object} ContentLengthMiddlewareOptions
* @prop {number} [delayCycles=2]
* Delays writing to stream by setTimeout cycles when piping.
* If `.end()` is called on the same event loop as write, then the
* content length can be still calculated despite receiving data in chunks.
* @prop {boolean} [overrideHeader=false]
* Always replace `Content-Length` header
*/
export default class ContentLengthMiddleware {
/** @param {ContentLengthMiddlewareOptions} [options] */
constructor(options = {}) {
this.delayCycles = options.delayCycles ?? 2;
this.overrideHeader = options.overrideHeader !== true;
this.finalizeResponse = this.finalizeResponse.bind(this);
}
/**
* @param {HttpResponse} response
* @return {void}
*/
addTransformStream(response) {
if (response.headersSent) return;
let { delayCycles } = this;
const { overrideHeader } = this;
if (response.headers['content-length'] && !overrideHeader) return;
let length = 0;
/** @type {Buffer[]} */
const pendingChunks = [];
response.pipes.push(new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
length += chunk.length;
if (!delayCycles) {
callback(null, chunk);
return;
}
pendingChunks.push(chunk);
// eslint-disable-next-line no-underscore-dangle, unicorn/consistent-function-scoping
let fn = () => this._flush(() => { /** noop */ });
for (let i = 0; i < delayCycles; i++) {
const prev = fn;
fn = () => setTimeout(prev);
}
fn();
callback();
},
flush(callback) {
for (const buffer of pendingChunks.splice(0, pendingChunks.length)) {
this.push(buffer);
}
delayCycles = 0;
callback?.();
},
final(callback) {
if (!response.headersSent) {
/**
* Any response message which "MUST NOT" include a message-body
* (such as the 1xx, 204, and 304 responses and any response to a HEAD request)
* is always terminated by the first empty line after the header fields,
* regardless of the entity-header fields present in the message.
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
*/
if ((response.status >= 100 && response.status < 200) || response.status === 204 || response.status === 304) {
if (overrideHeader) {
delete response.headers['content-length'];
}
} else if (overrideHeader === true || response.headers['content-length'] == null) {
response.headers['content-length'] = length;
}
}
callback?.();
},
}));
}
/** @type {ResponseFinalizer} */
finalizeResponse(response) {
if (response.headersSent) return;
if (response.isStreaming) {
this.addTransformStream(response);
return;
}
if (!Buffer.isBuffer(response.body)) return;
if (!response.body.byteLength) return;
/**
* Any response message which "MUST NOT" include a message-body
* (such as the 1xx, 204, and 304 responses and any response to a HEAD request)
* is always terminated by the first empty line after the header fields,
* regardless of the entity-header fields present in the message.
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
*/
if (response.status === 204 || response.status === 304 || (response.status >= 100 && response.status < 200)) {
if (this.overrideHeader) {
delete response.headers['content-length'];
}
} else if (this.overrideHeader === true || response.headers['content-length'] == null) {
response.headers['content-length'] = response.body.byteLength;
}
}
/** @type {MiddlewareFunction} */
execute({ response }) {
response.finalizers.push(this.finalizeResponse);
}
}