/
ContentDecoderMiddleware.js
141 lines (122 loc) · 3.81 KB
/
ContentDecoderMiddleware.js
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { Transform } from 'node:stream';
import {
// @ts-expect-error Bad types
BrotliDecompress, Gunzip, Inflate,
} from 'node:zlib';
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
/**
* @typedef ContentDecoderMiddlewareOptions
* @prop {number} [chunkSize]
* @prop {boolean} [respondNotAcceptable=false]
*/
const CONTINUE = true;
/**
* Implements `Accept-Encoding`
* https://tools.ietf.org/html/rfc7231#section-5.3.4
*/
export default class ContentDecoderMiddleware {
/** @param {ContentDecoderMiddlewareOptions} [options] */
constructor(options = {}) {
this.chunkSize = options.chunkSize;
this.respondNotAcceptable = options.respondNotAcceptable === true;
}
/** @type {MiddlewareFunction} */
execute({ request, response }) {
switch (request.method) {
case 'HEAD':
case 'GET':
return CONTINUE;
default:
}
// TODO: Use transforms
response.headers['accept-encoding'] = 'gzip, deflate, br';
const contentEncoding = request.headers['content-encoding'];
if (!contentEncoding) return CONTINUE;
switch (contentEncoding.trim().toLowerCase()) {
case '':
case 'identity':
return CONTINUE;
case 'gzip':
case 'br':
case 'deflate':
break;
default:
if (this.respondNotAcceptable) {
return 406;
}
return CONTINUE;
}
/** @type {import('stream').Readable} */
let inputStream;
// Don't built gZipStream until a read request is made
// By default, newDownstream <= inputStream
// On first read, newDownstream <= gZipStream <= inputStream
// Read request is intercepted by newDownstream
/** @type {import("zlib").Gunzip} */
let gzipStream;
let initialized = false;
const gzipOptions = { chunkSize: this.chunkSize };
const newDownstream = new Transform({
read: (...args) => {
if (!initialized) {
/** @type {import("zlib").Gzip} */
switch (contentEncoding) {
case 'deflate':
// @ts-expect-error Bad types
gzipStream = new Inflate(gzipOptions);
break;
case 'gzip':
// @ts-expect-error Bad types
gzipStream = new Gunzip(gzipOptions);
break;
case 'br':
// @ts-expect-error Bad types
gzipStream = new BrotliDecompress(gzipOptions);
break;
default:
throw new Error('UNKNOWN_ENCODING');
}
// From newDownstream <= inputStream
// To newDownstream <= gzipStream < =inputStream
// Forward errors
gzipStream.on('error', (err) => inputStream.emit('error', err));
gzipStream.on('data', (chunk) => newDownstream.push(chunk));
inputStream.on('end', () => gzipStream.end());
gzipStream.on('end', () => {
newDownstream.push(null);
if (newDownstream.readable) {
newDownstream.end();
}
});
if (inputStream.pause()) inputStream.resume();
initialized = true;
}
Transform.prototype._read.call(this, ...args);
},
transform: (chunk, chunkEncoding, callback) => {
gzipStream.write(chunk, (err) => {
if (err) console.error(err);
callback(err);
});
},
flush: (callback) => {
if (gzipStream) {
gzipStream.flush(() => {
callback();
});
}
},
final: (callback) => {
if (gzipStream) {
gzipStream.end();
gzipStream.flush(() => {
callback();
});
}
},
});
newDownstream.tag = 'ContentDecoder';
inputStream = request.addDownstream(newDownstream, { autoPause: true });
return CONTINUE;
}
}