@boo-labs/server 0.0.0-stage → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +287 -2
- package/index.mjs +23 -0
- package/package.json +71 -4
- package/source/application/Kernel/index.mjs +1137 -0
- package/source/application/Server/index.mjs +1201 -0
- package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
- package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
- package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
- package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
- package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
- package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
- package/source/application/resolvers/TransportResolver/index.mjs +119 -0
- package/source/application/use-cases/CloseServer/index.mjs +85 -0
- package/source/application/use-cases/ListenServer/index.mjs +98 -0
- package/source/core/EventSystem/index.mjs +110 -0
- package/source/core/Exchange/ExchangeContext.mjs +291 -0
- package/source/core/Exchange/Telemetry.mjs +193 -0
- package/source/core/Exchange/index.mjs +604 -0
- package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
- package/source/core/IPCMain/IPCMain.mjs +507 -0
- package/source/core/IPCMain/IPCProtocol.mjs +441 -0
- package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
- package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
- package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
- package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
- package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
- package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
- package/source/core/Request/RequestHeaders.mjs +177 -0
- package/source/core/Request/index.mjs +805 -0
- package/source/core/Response/ResponseHeaders.mjs +402 -0
- package/source/core/Response/index.mjs +1370 -0
- package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
- package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
- package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
- package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
- package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
- package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
- package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
- package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
- package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
- package/source/core/routing/ExchangeLayer/index.mjs +254 -0
- package/source/core/routing/Pipeline/index.mjs +157 -0
- package/source/core/routing/Router/Route.mjs +184 -0
- package/source/core/routing/Router/index.mjs +523 -0
- package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
- package/source/core/routing/RouterRegister/index.mjs +150 -0
- package/source/core/routing/RouterSystem/index.mjs +225 -0
- package/source/core/value-objects/Address/index.mjs +281 -0
- package/source/core/value-objects/Endpoint/index.mjs +293 -0
- package/source/index.mjs +62 -0
- package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
- package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
- package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
- package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
- package/source/shared/Identifier/index.mjs +671 -0
- package/source/shared/constants/symbols/brands/index.mjs +45 -0
- package/source/shared/domain/UUIDV7/index.mjs +346 -0
- package/source/shared/errors/BaseError/index.mjs +157 -0
- package/source/shared/errors/IPCError/index.mjs +97 -0
- package/source/shared/errors/MiddlewareError/index.mjs +106 -0
- package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
- package/source/shared/errors/RequestError/index.mjs +116 -0
- package/source/shared/errors/ResponseError/index.mjs +140 -0
- package/source/shared/errors/RouterError/index.mjs +90 -0
- package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
- package/source/shared/errors/ServerError/index.mjs +127 -0
- package/source/shared/errors/WebSocketError/index.mjs +58 -0
- package/source/shared/outcome/ErrorSchema.mjs +361 -0
- package/source/shared/outcome/Result/index.mjs +239 -0
- package/source/shared/outcome/Validation/index.mjs +208 -0
- package/source/shared/validations/Middleware/index.mjs +160 -0
- package/source/shared/validations/PathnamePattern/index.mjs +256 -0
- package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
- package/source/shared/validations/Server/Router/index.mjs +68 -0
- package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
- package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
- package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
- package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
- package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
- package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
- package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
- package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
- package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
- package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
- package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
- package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
- package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
- package/types/index.d.ts +719 -0
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Defines the Response abstraction.
|
|
4
|
+
*
|
|
5
|
+
* @description Header values must remain compatible with the HTTP field-value ABNF.
|
|
6
|
+
*
|
|
7
|
+
* This class does not perform RFC 5987 encoding. Callers that need to emit
|
|
8
|
+
* non-ASCII content (for example a UTF-8 filename in `Content-Disposition`)
|
|
9
|
+
* are responsible for encoding the value themselves before passing it to
|
|
10
|
+
* this class.
|
|
11
|
+
*
|
|
12
|
+
* @module Response
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
16
|
+
// » IMPORT NATIVE NODEJS MODULES
|
|
17
|
+
import { pipeline } from 'node:stream/promises';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { Readable } from 'node:stream';
|
|
20
|
+
|
|
21
|
+
// » IMPORT LOCAL MODULES
|
|
22
|
+
import ResponseError from '#shared/errors/ResponseError';
|
|
23
|
+
import ResponseHeaders from './ResponseHeaders.mjs';
|
|
24
|
+
|
|
25
|
+
// ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
|
+
/**
|
|
27
|
+
* Represents a native Node.js HTTP/1.1 server response.
|
|
28
|
+
*
|
|
29
|
+
* @typedef {import('node:http').ServerResponse} HttpServerResponse
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Represents a native Node.js HTTP/2 server response.
|
|
34
|
+
*
|
|
35
|
+
* @typedef {import('node:http2').Http2ServerResponse} Http2ServerResponse
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Represents a native Node.js readable stream.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {import('node:stream').Readable} NodeReadable
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Represents a native Node.js transform stream.
|
|
46
|
+
*
|
|
47
|
+
* @typedef {import('node:stream').Transform} NodeTransform
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Represents a native Node.js HTTP/1.1 or HTTP/2 server response.
|
|
52
|
+
*
|
|
53
|
+
* @typedef {HttpServerResponse|Http2ServerResponse} ServerResponse
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Represents the current state of the response body.
|
|
58
|
+
*
|
|
59
|
+
* The union is discriminated by the `kind` field, which allows the type
|
|
60
|
+
* system to narrow the `value` field to the appropriate type for each state.
|
|
61
|
+
*
|
|
62
|
+
* @typedef {{ kind: 'none', value: null } |{ kind: 'buffer', value: string|Uint8Array }|{ kind: 'stream', value: NodeReadable }} ResponseBody
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Identifies the kind of body currently described by a response.
|
|
67
|
+
*
|
|
68
|
+
* @typedef {'none'|'buffer'|'stream'} BodyKind
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Handles response processing immediately before it is committed.
|
|
73
|
+
*
|
|
74
|
+
* @typedef {(response: Response) => void} CommitHook
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handles errors produced while committing a response.
|
|
79
|
+
*
|
|
80
|
+
* @typedef {(error: Error) => void} CommitErrorHook
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Deferred source and transform used when the response body is wrapped.
|
|
85
|
+
*
|
|
86
|
+
* The pair is treated as a single unit of state: either no wrap is pending
|
|
87
|
+
* (`null`) or both halves are present. There is no partially built state.
|
|
88
|
+
*
|
|
89
|
+
* @typedef {object} BodyWrap
|
|
90
|
+
* @property {NodeReadable} source - Readable piped into the transform.
|
|
91
|
+
* @property {NodeTransform} transform - Transform applied to the body.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Defines the lifecycle hooks and options supported by a response.
|
|
96
|
+
*
|
|
97
|
+
* @typedef {object} ResponseOptions
|
|
98
|
+
* @property {CommitHook} [onCommit] - The hook invoked when response commitment begins. Errors thrown by this hook are forwarded to `onCommitError` and do not prevent the response from being sent.
|
|
99
|
+
* @property {CommitErrorHook} [onCommitError] - The hook invoked when response commitment produces an error.
|
|
100
|
+
* @property {boolean} [etag=false] - Whether to generate a weak ETag for buffered bodies.
|
|
101
|
+
* @property {string} [defaultCharset] - Charset appended to eligible Content-Type headers when none is set.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
// ━━ CONSTANTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
105
|
+
/**
|
|
106
|
+
* Maps HTTP status codes to their standard reason phrases.
|
|
107
|
+
*
|
|
108
|
+
* The map represents the set of final status codes supported by this
|
|
109
|
+
* abstraction. Informational (`1xx`) codes and reserved/unused codes such
|
|
110
|
+
* as `306` are intentionally excluded.
|
|
111
|
+
*
|
|
112
|
+
* @type {Map<number, string>}
|
|
113
|
+
*/
|
|
114
|
+
const HTTP_STATUS_CODES = new Map([
|
|
115
|
+
[200, 'OK'],
|
|
116
|
+
[201, 'Created'],
|
|
117
|
+
[202, 'Accepted'],
|
|
118
|
+
[203, 'Non-Authoritative Information'],
|
|
119
|
+
[204, 'No Content'],
|
|
120
|
+
[205, 'Reset Content'],
|
|
121
|
+
[206, 'Partial Content'],
|
|
122
|
+
[207, 'Multi-Status'],
|
|
123
|
+
[208, 'Already Reported'],
|
|
124
|
+
[226, 'IM Used'],
|
|
125
|
+
[300, 'Multiple Choices'],
|
|
126
|
+
[301, 'Moved Permanently'],
|
|
127
|
+
[302, 'Found'],
|
|
128
|
+
[303, 'See Other'],
|
|
129
|
+
[304, 'Not Modified'],
|
|
130
|
+
[305, 'Use Proxy'],
|
|
131
|
+
[307, 'Temporary Redirect'],
|
|
132
|
+
[308, 'Permanent Redirect'],
|
|
133
|
+
[400, 'Bad Request'],
|
|
134
|
+
[401, 'Unauthorized'],
|
|
135
|
+
[402, 'Payment Required'],
|
|
136
|
+
[403, 'Forbidden'],
|
|
137
|
+
[404, 'Not Found'],
|
|
138
|
+
[405, 'Method Not Allowed'],
|
|
139
|
+
[406, 'Not Acceptable'],
|
|
140
|
+
[407, 'Proxy Authentication Required'],
|
|
141
|
+
[408, 'Request Timeout'],
|
|
142
|
+
[409, 'Conflict'],
|
|
143
|
+
[410, 'Gone'],
|
|
144
|
+
[411, 'Length Required'],
|
|
145
|
+
[412, 'Precondition Failed'],
|
|
146
|
+
[413, 'Payload Too Large'],
|
|
147
|
+
[414, 'URI Too Long'],
|
|
148
|
+
[415, 'Unsupported Media Type'],
|
|
149
|
+
[416, 'Range Not Satisfiable'],
|
|
150
|
+
[417, 'Expectation Failed'],
|
|
151
|
+
[421, 'Misdirected Request'],
|
|
152
|
+
[422, 'Unprocessable Entity'],
|
|
153
|
+
[423, 'Locked'],
|
|
154
|
+
[424, 'Failed Dependency'],
|
|
155
|
+
[425, 'Too Early'],
|
|
156
|
+
[426, 'Upgrade Required'],
|
|
157
|
+
[428, 'Precondition Required'],
|
|
158
|
+
[429, 'Too Many Requests'],
|
|
159
|
+
[431, 'Request Header Fields Too Large'],
|
|
160
|
+
[451, 'Unavailable For Legal Reasons'],
|
|
161
|
+
[500, 'Internal Server Error'],
|
|
162
|
+
[501, 'Not Implemented'],
|
|
163
|
+
[502, 'Bad Gateway'],
|
|
164
|
+
[503, 'Service Unavailable'],
|
|
165
|
+
[504, 'Gateway Timeout'],
|
|
166
|
+
[505, 'HTTP Version Not Supported'],
|
|
167
|
+
[506, 'Variant Also Negotiates'],
|
|
168
|
+
[507, 'Insufficient Storage'],
|
|
169
|
+
[508, 'Loop Detected'],
|
|
170
|
+
[510, 'Not Extended'],
|
|
171
|
+
[511, 'Network Authentication Required'],
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Identifies the HTTP status codes supported by `Response.redirect()`.
|
|
176
|
+
*
|
|
177
|
+
* @type {Set<number>}
|
|
178
|
+
*/
|
|
179
|
+
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Defines the default HTTP status code for a new response.
|
|
183
|
+
*
|
|
184
|
+
* @type {number}
|
|
185
|
+
*/
|
|
186
|
+
const DEFAULT_STATUS_CODE = 200;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Identifies status codes that must not contain a response body.
|
|
190
|
+
*
|
|
191
|
+
* @type {Set<number>}
|
|
192
|
+
*/
|
|
193
|
+
const EMPTY_BODY_STATUS_CODES = new Set([204, 205, 304]);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Detects an existing `charset` parameter in a `Content-Type` header value.
|
|
197
|
+
*
|
|
198
|
+
* @type {RegExp}
|
|
199
|
+
*/
|
|
200
|
+
const CHARSET_PARAMETER_PATTERN = /;\s*charset\s*=/i;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Media types eligible for default charset application.
|
|
204
|
+
*
|
|
205
|
+
* @type {Set<string>}
|
|
206
|
+
*/
|
|
207
|
+
const CHARSET_ELIGIBLE_MEDIA_TYPES = new Set([
|
|
208
|
+
'application/json',
|
|
209
|
+
'application/xml',
|
|
210
|
+
'application/javascript',
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
// ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
214
|
+
/**
|
|
215
|
+
* Represents an outgoing HTTP response.
|
|
216
|
+
*
|
|
217
|
+
* @classdesc A response is built progressively by configuring its status,
|
|
218
|
+
* headers, and body. Body description methods do not send anything to the client.
|
|
219
|
+
* Only `end()` materializes the configured response and sends it through
|
|
220
|
+
* the underlying server response.
|
|
221
|
+
*
|
|
222
|
+
* A response can describe at most one body. Once a body has been described,
|
|
223
|
+
* another body description method throws `BODY_ALREADY_DESCRIBED`.
|
|
224
|
+
*
|
|
225
|
+
* Once a body has been described and a `Content-Type` header has been
|
|
226
|
+
* established, that header becomes immutable for the rest of the response
|
|
227
|
+
* lifecycle. This prevents a body description helper from being followed
|
|
228
|
+
* by an incompatible content type.
|
|
229
|
+
*
|
|
230
|
+
* Switching to an empty-body status code (204, 205, 304) after a body has
|
|
231
|
+
* already been described is not allowed and throws `BODY_NOT_ALLOWED`.
|
|
232
|
+
*
|
|
233
|
+
* Informational status codes (`1xx`) are not supported by this abstraction.
|
|
234
|
+
* They are protocol-level messages that must be produced by dedicated
|
|
235
|
+
* native APIs (`writeContinue()`, `writeEarlyHints()`) or by the transport
|
|
236
|
+
* upgrade flow, not by a handler response.
|
|
237
|
+
*/
|
|
238
|
+
class Response {
|
|
239
|
+
/**
|
|
240
|
+
* The current HTTP status code.
|
|
241
|
+
*
|
|
242
|
+
* @type {number}
|
|
243
|
+
*/
|
|
244
|
+
#statusCode = DEFAULT_STATUS_CODE;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The response headers.
|
|
248
|
+
*
|
|
249
|
+
* @type {ResponseHeaders}
|
|
250
|
+
*/
|
|
251
|
+
#headers = new ResponseHeaders();
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The current body state, discriminated by `kind`.
|
|
255
|
+
*
|
|
256
|
+
* @type {ResponseBody}
|
|
257
|
+
*/
|
|
258
|
+
#body = { kind: 'none', value: null };
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Whether the response has been committed.
|
|
262
|
+
*
|
|
263
|
+
* @type {boolean}
|
|
264
|
+
*/
|
|
265
|
+
#committed = false;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The underlying server response.
|
|
269
|
+
*
|
|
270
|
+
* @type {ServerResponse}
|
|
271
|
+
*/
|
|
272
|
+
#serverResponse;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The hook invoked when response commitment begins.
|
|
276
|
+
*
|
|
277
|
+
* @type {CommitHook|null}
|
|
278
|
+
*/
|
|
279
|
+
#onCommit = null;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The hook invoked when response commitment produces an error.
|
|
283
|
+
*
|
|
284
|
+
* @type {CommitErrorHook|null}
|
|
285
|
+
*/
|
|
286
|
+
#onCommitError = null;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Whether to generate a weak ETag for buffered bodies.
|
|
290
|
+
*
|
|
291
|
+
* @type {boolean}
|
|
292
|
+
*/
|
|
293
|
+
#etag = false;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Charset appended to eligible `Content-Type` headers when none is set.
|
|
297
|
+
*
|
|
298
|
+
* @type {string|null}
|
|
299
|
+
*/
|
|
300
|
+
#defaultCharset = null;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Deferred source and transform used when the response body is wrapped.
|
|
304
|
+
*
|
|
305
|
+
* `null` when no wrap is pending. Set only by `#wrapBody`; never exposed
|
|
306
|
+
* publicly. Holds both halves of the pipeline stage as a single unit so
|
|
307
|
+
* they cannot desynchronize.
|
|
308
|
+
*
|
|
309
|
+
* @type {BodyWrap|null}
|
|
310
|
+
*/
|
|
311
|
+
#wrap = null;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Creates a response bound to a native Node.js server response.
|
|
315
|
+
*
|
|
316
|
+
* The optional commit hooks allow the surrounding `Exchange` to inject
|
|
317
|
+
* behavior immediately before materialization and observe errors produced
|
|
318
|
+
* during that process without coupling `Response` to that behavior.
|
|
319
|
+
*
|
|
320
|
+
* @param {ServerResponse} serverResponse - The native Node.js server response.
|
|
321
|
+
* @param {ResponseOptions} [options] - The response lifecycle hooks and options.
|
|
322
|
+
* @example
|
|
323
|
+
* ```js
|
|
324
|
+
* const response = new Response(serverResponse, {
|
|
325
|
+
* onCommit: (response) => telemetry.apply(response),
|
|
326
|
+
* onCommitError: (error) => telemetry.recordError(error),
|
|
327
|
+
* etag: true,
|
|
328
|
+
* defaultCharset: 'utf-8',
|
|
329
|
+
* });
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
constructor(serverResponse, options = {}) {
|
|
333
|
+
this.#serverResponse = serverResponse;
|
|
334
|
+
this.#onCommit = typeof options.onCommit === 'function' ? options.onCommit : null;
|
|
335
|
+
this.#onCommitError =
|
|
336
|
+
typeof options.onCommitError === 'function' ? options.onCommitError : null;
|
|
337
|
+
this.#etag = options.etag === true;
|
|
338
|
+
this.#defaultCharset =
|
|
339
|
+
typeof options.defaultCharset === 'string' && options.defaultCharset.length > 0
|
|
340
|
+
? options.defaultCharset
|
|
341
|
+
: null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The HTTP status code configured for the response.
|
|
346
|
+
*
|
|
347
|
+
* @type {number}
|
|
348
|
+
*/
|
|
349
|
+
get statusCode() {
|
|
350
|
+
return this.#statusCode;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The standard reason phrase associated with the configured status code.
|
|
355
|
+
*
|
|
356
|
+
* Returns an empty string when the status code has no mapped reason phrase.
|
|
357
|
+
*
|
|
358
|
+
* @type {string}
|
|
359
|
+
*/
|
|
360
|
+
get statusText() {
|
|
361
|
+
return HTTP_STATUS_CODES.get(this.#statusCode) ?? '';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Whether the response has already entered the committed state.
|
|
366
|
+
*
|
|
367
|
+
* A response is considered committed once `end()` has begun materializing
|
|
368
|
+
* it. This means the response can no longer be modified through the public
|
|
369
|
+
* API. It does not mean that the client has received the response; the
|
|
370
|
+
* transport may still be writing bytes to the socket when this getter
|
|
371
|
+
* returns `true`.
|
|
372
|
+
*
|
|
373
|
+
* @type {boolean}
|
|
374
|
+
*/
|
|
375
|
+
get isCommitted() {
|
|
376
|
+
return this.#committed;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Describes the current body state of the response.
|
|
381
|
+
*
|
|
382
|
+
* - `'none'`: no body has been described.
|
|
383
|
+
* - `'buffer'`: a buffered body has been described.
|
|
384
|
+
* - `'stream'`: a stream body has been described.
|
|
385
|
+
*
|
|
386
|
+
* @type {BodyKind}
|
|
387
|
+
*/
|
|
388
|
+
get bodyKind() {
|
|
389
|
+
return this.#body.kind;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Byte length of the described buffered body, or `undefined` when the
|
|
394
|
+
* body is absent or is a stream.
|
|
395
|
+
*
|
|
396
|
+
* After `wrapBody()`, the body is a stream and this getter returns
|
|
397
|
+
* `undefined`.
|
|
398
|
+
*
|
|
399
|
+
* @type {number|undefined}
|
|
400
|
+
*/
|
|
401
|
+
get bodyLength() {
|
|
402
|
+
if (this.#body.kind !== 'buffer') {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const value = this.#body.value;
|
|
407
|
+
return value instanceof Uint8Array ? value.byteLength : Buffer.byteLength(value);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Sets the HTTP status code.
|
|
412
|
+
*
|
|
413
|
+
* The status code must be a supported final HTTP status code. Informational
|
|
414
|
+
* status codes (`1xx`) are not supported by this abstraction. The status
|
|
415
|
+
* cannot be changed after the response has been committed.
|
|
416
|
+
*
|
|
417
|
+
* Switching to an empty-body status code (204, 205, 304) after a body has
|
|
418
|
+
* already been described is not allowed.
|
|
419
|
+
*
|
|
420
|
+
* @param {number} code - The HTTP status code.
|
|
421
|
+
* @returns {this} The response instance.
|
|
422
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
423
|
+
* @throws {ResponseError} If the status code is invalid or unsupported.
|
|
424
|
+
* @throws {ResponseError} If an empty-body status is set after a body was described.
|
|
425
|
+
* @example
|
|
426
|
+
* ```js
|
|
427
|
+
* response.status(201);
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
status(code) {
|
|
431
|
+
this.#assertNotCommitted();
|
|
432
|
+
|
|
433
|
+
if (!Number.isInteger(code) || !HTTP_STATUS_CODES.has(code)) {
|
|
434
|
+
throw new ResponseError('INVALID_STATUS_CODE', {
|
|
435
|
+
values: { statusCode: code },
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (EMPTY_BODY_STATUS_CODES.has(code) && this.#body.kind !== 'none') {
|
|
440
|
+
throw new ResponseError('BODY_NOT_ALLOWED', {
|
|
441
|
+
values: { statusCode: code },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
this.#statusCode = code;
|
|
446
|
+
return this;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Sets a response header.
|
|
451
|
+
*
|
|
452
|
+
* Header names are normalized to lowercase internally. Calling this method
|
|
453
|
+
* replaces any existing value for the same header name.
|
|
454
|
+
*
|
|
455
|
+
* Once a body has been described and a `Content-Type` header has been
|
|
456
|
+
* established, that header cannot be replaced.
|
|
457
|
+
*
|
|
458
|
+
* Header values must be compatible with the HTTP field-value syntax.
|
|
459
|
+
* Non-ASCII encoding (RFC 5987) is the caller's responsibility.
|
|
460
|
+
*
|
|
461
|
+
* @param {string} name - The HTTP header name.
|
|
462
|
+
* @param {string|number|(string|number)[]} value - The HTTP header value.
|
|
463
|
+
* @returns {this} The response instance.
|
|
464
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
465
|
+
* @throws {ResponseError} If the header name or value is invalid.
|
|
466
|
+
* @throws {ResponseError} If `Content-Type` is already locked.
|
|
467
|
+
* @example
|
|
468
|
+
* ```js
|
|
469
|
+
* response.setHeader('Content-Type', 'application/json');
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
setHeader(name, value) {
|
|
473
|
+
this.#assertNotCommitted();
|
|
474
|
+
this.#headers.set(name, value);
|
|
475
|
+
return this;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Sets multiple response headers.
|
|
480
|
+
*
|
|
481
|
+
* Existing values are replaced when the supplied object contains the same
|
|
482
|
+
* header name.
|
|
483
|
+
*
|
|
484
|
+
* @param {Record<string, string|number|(string|number)[]>} headers - The response headers to set.
|
|
485
|
+
* @returns {this} The response instance.
|
|
486
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
487
|
+
* @throws {ResponseError} If the headers value is not an object.
|
|
488
|
+
* @throws {ResponseError} If any header name or value is invalid.
|
|
489
|
+
* @throws {ResponseError} If `Content-Type` is already locked.
|
|
490
|
+
* @example
|
|
491
|
+
* ```js
|
|
492
|
+
* response.setHeaders({
|
|
493
|
+
* 'Content-Type': 'application/json',
|
|
494
|
+
* 'Cache-Control': 'no-store',
|
|
495
|
+
* });
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
setHeaders(headers) {
|
|
499
|
+
this.#assertNotCommitted();
|
|
500
|
+
this.#headers.setMany(headers);
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Appends a value to an existing header or creates the header when it does
|
|
506
|
+
* not exist.
|
|
507
|
+
*
|
|
508
|
+
* Existing scalar values are converted to an array when another value is
|
|
509
|
+
* appended.
|
|
510
|
+
*
|
|
511
|
+
* @param {string} name - The HTTP header name.
|
|
512
|
+
* @param {string|number} value - The value to append.
|
|
513
|
+
* @returns {this} The response instance.
|
|
514
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
515
|
+
* @throws {ResponseError} If the header name or value is invalid.
|
|
516
|
+
* @throws {ResponseError} If `Content-Type` is already locked.
|
|
517
|
+
* @example
|
|
518
|
+
* ```js
|
|
519
|
+
* response
|
|
520
|
+
* .appendHeader('Set-Cookie', 'session=abc')
|
|
521
|
+
* .appendHeader('Set-Cookie', 'theme=dark');
|
|
522
|
+
* ```
|
|
523
|
+
*/
|
|
524
|
+
appendHeader(name, value) {
|
|
525
|
+
this.#assertNotCommitted();
|
|
526
|
+
this.#headers.append(name, value);
|
|
527
|
+
return this;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Returns the value of a response header.
|
|
532
|
+
*
|
|
533
|
+
* When the header value is an array, a defensive copy is returned so that
|
|
534
|
+
* the caller cannot mutate the internal response state through the result.
|
|
535
|
+
*
|
|
536
|
+
* @param {string} name - The HTTP header name.
|
|
537
|
+
* @returns {string|number|(string|number)[]|undefined} The header value, or `undefined` when the header does not exist.
|
|
538
|
+
* @example
|
|
539
|
+
* ```js
|
|
540
|
+
* const contentType = response.getHeader('Content-Type');
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
getHeader(name) {
|
|
544
|
+
return this.#headers.get(name);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Checks whether a response header exists.
|
|
549
|
+
*
|
|
550
|
+
* @param {string} name - The HTTP header name.
|
|
551
|
+
* @returns {boolean} `true` when the header exists; otherwise, `false`.
|
|
552
|
+
* @example
|
|
553
|
+
* ```js
|
|
554
|
+
* if (response.hasHeader('ETag')) {
|
|
555
|
+
* // The response already defines an ETag.
|
|
556
|
+
* }
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
hasHeader(name) {
|
|
560
|
+
return this.#headers.has(name);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Removes a response header.
|
|
565
|
+
*
|
|
566
|
+
* Removing a header that does not exist has no effect.
|
|
567
|
+
*
|
|
568
|
+
* @param {string} name - The HTTP header name.
|
|
569
|
+
* @returns {this} The response instance.
|
|
570
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
571
|
+
* @throws {ResponseError} If `Content-Type` is already locked.
|
|
572
|
+
* @example
|
|
573
|
+
* ```js
|
|
574
|
+
* response.removeHeader('X-Debug');
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
removeHeader(name) {
|
|
578
|
+
this.#assertNotCommitted();
|
|
579
|
+
this.#headers.remove(name);
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Returns a frozen copy of the configured response headers.
|
|
585
|
+
*
|
|
586
|
+
* @returns {Readonly<Record<string, string|number|(string|number)[]>>} A frozen copy of the configured headers.
|
|
587
|
+
* @example
|
|
588
|
+
* ```js
|
|
589
|
+
* const headers = response.getHeaders();
|
|
590
|
+
* ```
|
|
591
|
+
*/
|
|
592
|
+
getHeaders() {
|
|
593
|
+
return Object.freeze(this.#headers.toObject());
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Describes a JSON response body.
|
|
598
|
+
*
|
|
599
|
+
* This method does not send the response. It serializes the supplied value,
|
|
600
|
+
* sets a default JSON content type when one has not already been configured,
|
|
601
|
+
* and stores the serialized body until `end()` is called.
|
|
602
|
+
*
|
|
603
|
+
* The method throws `INVALID_JSON` when JSON serialization does not produce
|
|
604
|
+
* a JSON string or when serialization itself fails. `null` is a valid JSON
|
|
605
|
+
* value and is preserved.
|
|
606
|
+
*
|
|
607
|
+
* @param {*} data - The value to serialize as JSON.
|
|
608
|
+
* @returns {this} The response instance.
|
|
609
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
610
|
+
* @throws {ResponseError} If another body has already been described.
|
|
611
|
+
* @throws {ResponseError} If the current status does not allow a body.
|
|
612
|
+
* @throws {ResponseError} If the value cannot be serialized as JSON.
|
|
613
|
+
* @example
|
|
614
|
+
* ```js
|
|
615
|
+
* response
|
|
616
|
+
* .status(201)
|
|
617
|
+
* .json({ id: 42, name: 'Alice' })
|
|
618
|
+
* .end();
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
json(data) {
|
|
622
|
+
this.#assertNotCommitted();
|
|
623
|
+
this.#assertBodyNotDescribed();
|
|
624
|
+
this.#assertBodyAllowed();
|
|
625
|
+
|
|
626
|
+
const body = this.#serializeJson(data);
|
|
627
|
+
|
|
628
|
+
this.#headers.ensureContentType('application/json');
|
|
629
|
+
this.#body = { kind: 'buffer', value: body };
|
|
630
|
+
this.#headers.lockContentType();
|
|
631
|
+
return this;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Describes an HTML response body.
|
|
636
|
+
*
|
|
637
|
+
* This method does not send the response. It sets a default HTML content
|
|
638
|
+
* type when one has not already been configured.
|
|
639
|
+
*
|
|
640
|
+
* @param {string} content - The HTML content.
|
|
641
|
+
* @returns {this} The response instance.
|
|
642
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
643
|
+
* @throws {ResponseError} If another body has already been described.
|
|
644
|
+
* @throws {ResponseError} If the current status does not allow a body.
|
|
645
|
+
* @throws {ResponseError} If the content is not a string.
|
|
646
|
+
* @example
|
|
647
|
+
* ```js
|
|
648
|
+
* response
|
|
649
|
+
* .status(200)
|
|
650
|
+
* .html('<h1>Hello</h1>')
|
|
651
|
+
* .end();
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
html(content) {
|
|
655
|
+
this.#assertNotCommitted();
|
|
656
|
+
this.#assertBodyNotDescribed();
|
|
657
|
+
this.#assertBodyAllowed();
|
|
658
|
+
this.#assertStringBody(content);
|
|
659
|
+
|
|
660
|
+
this.#headers.ensureContentType('text/html');
|
|
661
|
+
this.#body = { kind: 'buffer', value: content };
|
|
662
|
+
this.#headers.lockContentType();
|
|
663
|
+
return this;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Describes a plain-text response body.
|
|
668
|
+
*
|
|
669
|
+
* This method does not send the response. It sets a default plain-text
|
|
670
|
+
* content type when one has not already been configured.
|
|
671
|
+
*
|
|
672
|
+
* @param {string} content - The plain-text content.
|
|
673
|
+
* @returns {this} The response instance.
|
|
674
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
675
|
+
* @throws {ResponseError} If another body has already been described.
|
|
676
|
+
* @throws {ResponseError} If the current status does not allow a body.
|
|
677
|
+
* @throws {ResponseError} If the content is not a string.
|
|
678
|
+
* @example
|
|
679
|
+
* ```js
|
|
680
|
+
* response
|
|
681
|
+
* .text('Hello, world!')
|
|
682
|
+
* .end();
|
|
683
|
+
* ```
|
|
684
|
+
*/
|
|
685
|
+
text(content) {
|
|
686
|
+
this.#assertNotCommitted();
|
|
687
|
+
this.#assertBodyNotDescribed();
|
|
688
|
+
this.#assertBodyAllowed();
|
|
689
|
+
this.#assertStringBody(content);
|
|
690
|
+
|
|
691
|
+
this.#headers.ensureContentType('text/plain');
|
|
692
|
+
this.#body = { kind: 'buffer', value: content };
|
|
693
|
+
this.#headers.lockContentType();
|
|
694
|
+
return this;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Describes an arbitrary string or binary response body.
|
|
699
|
+
*
|
|
700
|
+
* This method does not send the response and does not assign a default
|
|
701
|
+
* content type.
|
|
702
|
+
*
|
|
703
|
+
* @param {string|Uint8Array} body - The response body. Defaults to an empty string.
|
|
704
|
+
* @returns {this} The response instance.
|
|
705
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
706
|
+
* @throws {ResponseError} If another body has already been described.
|
|
707
|
+
* @throws {ResponseError} If the current status does not allow a body.
|
|
708
|
+
* @throws {ResponseError} If the body is not a string or `Uint8Array`.
|
|
709
|
+
* @example
|
|
710
|
+
* ```js
|
|
711
|
+
* response
|
|
712
|
+
* .setHeader('Content-Type', 'application/octet-stream')
|
|
713
|
+
* .send(buffer)
|
|
714
|
+
* .end();
|
|
715
|
+
* ```
|
|
716
|
+
*/
|
|
717
|
+
send(body = '') {
|
|
718
|
+
this.#assertNotCommitted();
|
|
719
|
+
this.#assertBodyNotDescribed();
|
|
720
|
+
this.#assertBodyAllowed();
|
|
721
|
+
this.#assertBufferedBody(body);
|
|
722
|
+
|
|
723
|
+
this.#body = { kind: 'buffer', value: body };
|
|
724
|
+
this.#headers.lockContentType();
|
|
725
|
+
return this;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Describes a readable stream as the response body.
|
|
730
|
+
*
|
|
731
|
+
* This method does not send the response. The stream is connected to the
|
|
732
|
+
* underlying server response only when `end()` is called.
|
|
733
|
+
*
|
|
734
|
+
* @param {NodeReadable} stream - The readable stream.
|
|
735
|
+
* @param {string} [contentType] - The content type to use when none is already configured.
|
|
736
|
+
* @returns {this} The response instance.
|
|
737
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
738
|
+
* @throws {ResponseError} If another body has already been described.
|
|
739
|
+
* @throws {ResponseError} If the current status does not allow a body.
|
|
740
|
+
* @throws {ResponseError} If the stream is not a readable stream.
|
|
741
|
+
* @throws {ResponseError} If `contentType` is provided and is not a string.
|
|
742
|
+
* @example
|
|
743
|
+
* ```js
|
|
744
|
+
* response
|
|
745
|
+
* .stream(fileStream, 'application/pdf')
|
|
746
|
+
* .end();
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
749
|
+
stream(stream, contentType) {
|
|
750
|
+
this.#assertNotCommitted();
|
|
751
|
+
this.#assertBodyNotDescribed();
|
|
752
|
+
this.#assertBodyAllowed();
|
|
753
|
+
|
|
754
|
+
if (
|
|
755
|
+
stream === null ||
|
|
756
|
+
typeof stream !== 'object' ||
|
|
757
|
+
typeof stream.pipe !== 'function' ||
|
|
758
|
+
typeof stream.on !== 'function' ||
|
|
759
|
+
stream.readable === false
|
|
760
|
+
) {
|
|
761
|
+
throw new ResponseError('INVALID_STREAM');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (contentType !== undefined) {
|
|
765
|
+
this.#headers.ensureContentType(contentType);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
this.#body = { kind: 'stream', value: stream };
|
|
769
|
+
this.#headers.lockContentType();
|
|
770
|
+
return this;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Describes an HTTP redirect response.
|
|
775
|
+
*
|
|
776
|
+
* The redirect status must be one of the supported redirect status codes:
|
|
777
|
+
* `301`, `302`, `303`, `307`, or `308`. The method does not assign a content
|
|
778
|
+
* type because the redirect does not require a response body.
|
|
779
|
+
*
|
|
780
|
+
* Internally, the response is marked as having a buffered body to prevent
|
|
781
|
+
* another body description from being added after the redirect is configured.
|
|
782
|
+
*
|
|
783
|
+
* @param {string} url - The redirect target.
|
|
784
|
+
* @param {number} statusCode - The redirect status code. Defaults to `302`.
|
|
785
|
+
* @returns {this} The response instance.
|
|
786
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
787
|
+
* @throws {ResponseError} If another body has already been described.
|
|
788
|
+
* @throws {ResponseError} If the URL is empty or not a string.
|
|
789
|
+
* @throws {ResponseError} If the status code is not a supported redirect status.
|
|
790
|
+
* @example
|
|
791
|
+
* ```js
|
|
792
|
+
* response
|
|
793
|
+
* .redirect('/login')
|
|
794
|
+
* .end();
|
|
795
|
+
* ```
|
|
796
|
+
*
|
|
797
|
+
* @example
|
|
798
|
+
* ```js
|
|
799
|
+
* response
|
|
800
|
+
* .redirect('/new-location', 301)
|
|
801
|
+
* .end();
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
redirect(url, statusCode = 302) {
|
|
805
|
+
this.#assertNotCommitted();
|
|
806
|
+
this.#assertBodyNotDescribed();
|
|
807
|
+
|
|
808
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
809
|
+
throw new ResponseError('INVALID_REDIRECT_URL');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (!REDIRECT_STATUS_CODES.has(statusCode)) {
|
|
813
|
+
throw new ResponseError('INVALID_REDIRECT_STATUS', {
|
|
814
|
+
values: { statusCode },
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
this.status(statusCode);
|
|
819
|
+
this.setHeader('Location', url);
|
|
820
|
+
|
|
821
|
+
this.#body = { kind: 'buffer', value: '' };
|
|
822
|
+
this.#headers.lockContentType();
|
|
823
|
+
return this;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Adds one or more field names to the `Vary` response header.
|
|
828
|
+
*
|
|
829
|
+
* Duplicate values are ignored. Field names are normalized to lowercase.
|
|
830
|
+
*
|
|
831
|
+
* @param {...string} fields - Header field names to add to `Vary`.
|
|
832
|
+
* @returns {this} The response instance.
|
|
833
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
834
|
+
* @throws {TypeError} If any field is not a non-empty string.
|
|
835
|
+
* @example
|
|
836
|
+
* ```js
|
|
837
|
+
* response.vary('Accept', 'Accept-Encoding');
|
|
838
|
+
* response.vary('Origin');
|
|
839
|
+
* ```
|
|
840
|
+
*/
|
|
841
|
+
vary(...fields) {
|
|
842
|
+
this.#assertNotCommitted();
|
|
843
|
+
|
|
844
|
+
if (fields.length === 0) {
|
|
845
|
+
return this;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const existing = this.#headers.get('vary');
|
|
849
|
+
const values = new Set();
|
|
850
|
+
|
|
851
|
+
if (typeof existing === 'string') {
|
|
852
|
+
for (const part of existing.split(',')) {
|
|
853
|
+
const trimmed = part.trim().toLowerCase();
|
|
854
|
+
if (trimmed) values.add(trimmed);
|
|
855
|
+
}
|
|
856
|
+
} else if (Array.isArray(existing)) {
|
|
857
|
+
for (const item of existing) {
|
|
858
|
+
const trimmed = String(item).trim().toLowerCase();
|
|
859
|
+
if (trimmed) values.add(trimmed);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
for (const field of fields) {
|
|
864
|
+
if (typeof field !== 'string' || field.trim().length === 0) {
|
|
865
|
+
throw new TypeError('Vary field must be a non-empty string');
|
|
866
|
+
}
|
|
867
|
+
values.add(field.trim().toLowerCase());
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return this.setHeader('Vary', [...values].join(', '));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Configures a public cache lifetime using `Cache-Control`.
|
|
875
|
+
*
|
|
876
|
+
* @param {number} seconds - The maximum cache lifetime in seconds.
|
|
877
|
+
* @returns {this} The response instance.
|
|
878
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
879
|
+
* @throws {ResponseError} If the duration is not a non-negative integer.
|
|
880
|
+
* @example
|
|
881
|
+
* ```js
|
|
882
|
+
* response
|
|
883
|
+
* .cache(3600)
|
|
884
|
+
* .json({ message: 'cached' })
|
|
885
|
+
* .end();
|
|
886
|
+
* ```
|
|
887
|
+
*/
|
|
888
|
+
cache(seconds) {
|
|
889
|
+
this.#assertNotCommitted();
|
|
890
|
+
|
|
891
|
+
if (!Number.isInteger(seconds) || seconds < 0) {
|
|
892
|
+
throw new ResponseError('INVALID_CACHE_DURATION');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return this.setHeader('Cache-Control', `public, max-age=${seconds}`);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Disables client and intermediary caching for the response.
|
|
900
|
+
*
|
|
901
|
+
* @returns {this} The response instance.
|
|
902
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
903
|
+
* @example
|
|
904
|
+
* ```js
|
|
905
|
+
* response
|
|
906
|
+
* .noCache()
|
|
907
|
+
* .json({ message: 'private data' })
|
|
908
|
+
* .end();
|
|
909
|
+
* ```
|
|
910
|
+
*/
|
|
911
|
+
noCache() {
|
|
912
|
+
this.#assertNotCommitted();
|
|
913
|
+
|
|
914
|
+
return this.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
|
915
|
+
.setHeader('Pragma', 'no-cache')
|
|
916
|
+
.setHeader('Expires', '0');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Materializes and sends the configured response.
|
|
921
|
+
*
|
|
922
|
+
* The method is idempotent. The first invocation runs the optional commit
|
|
923
|
+
* hook, materializes the configured response, and marks it as committed.
|
|
924
|
+
* Subsequent invocations return the same response without sending again.
|
|
925
|
+
*
|
|
926
|
+
* Errors produced by the commit hook, by the underlying transport calls,
|
|
927
|
+
* or by a response stream are forwarded to the optional `onCommitError`
|
|
928
|
+
* hook. Those errors are considered handled by that hook; `Response` does
|
|
929
|
+
* not retry or otherwise recover the operation.
|
|
930
|
+
*
|
|
931
|
+
* Note: if a stream body fails after headers have been sent, the response
|
|
932
|
+
* to the client is already in progress and cannot be reverted to a `500`
|
|
933
|
+
* status. The error is forwarded to `onCommitError` for observability only.
|
|
934
|
+
*
|
|
935
|
+
* @returns {this} The response instance.
|
|
936
|
+
* @example
|
|
937
|
+
* ```js
|
|
938
|
+
* response
|
|
939
|
+
* .status(200)
|
|
940
|
+
* .json({ message: 'ok' })
|
|
941
|
+
* .end();
|
|
942
|
+
* ```
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* ```js
|
|
946
|
+
* response.end();
|
|
947
|
+
* response.end();
|
|
948
|
+
* ```
|
|
949
|
+
*/
|
|
950
|
+
end() {
|
|
951
|
+
if (this.#committed) {
|
|
952
|
+
return this;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (this.#onCommit) {
|
|
956
|
+
try {
|
|
957
|
+
this.#onCommit(this);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
this.#handleCommitError(this.#normalizeError(error));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
if (EMPTY_BODY_STATUS_CODES.has(this.#statusCode)) {
|
|
965
|
+
if (this.#statusCode === 205) {
|
|
966
|
+
this.#headers.set('content-length', '0');
|
|
967
|
+
} else {
|
|
968
|
+
this.#headers.remove('content-length');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
this.#writeHead();
|
|
972
|
+
this.#committed = true;
|
|
973
|
+
this.#serverResponse.end();
|
|
974
|
+
return this;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (this.#body.kind === 'stream') {
|
|
978
|
+
const stream = this.#body.value;
|
|
979
|
+
|
|
980
|
+
this.#writeHead();
|
|
981
|
+
this.#committed = true;
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Handles errors produced by the response pipeline.
|
|
985
|
+
*
|
|
986
|
+
* @param {unknown} error - Error produced by the response pipeline.
|
|
987
|
+
*/
|
|
988
|
+
const onError = error => {
|
|
989
|
+
this.#handleCommitError(this.#normalizeError(error));
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
if (this.#wrap !== null) {
|
|
993
|
+
pipeline(this.#wrap.source, this.#wrap.transform, this.#serverResponse).catch(onError);
|
|
994
|
+
} else {
|
|
995
|
+
pipeline(stream, this.#serverResponse).catch(onError);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return this;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const body = this.#body.value ?? '';
|
|
1002
|
+
const contentLength = body instanceof Uint8Array ? body.byteLength : Buffer.byteLength(body);
|
|
1003
|
+
|
|
1004
|
+
if (this.#body.kind !== 'none') {
|
|
1005
|
+
this.#headers.set('content-length', String(contentLength));
|
|
1006
|
+
} else if (!this.#headers.has('content-length')) {
|
|
1007
|
+
this.#headers.set('content-length', '0');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (this.#etag && !this.#headers.has('etag')) {
|
|
1011
|
+
this.#headers.set('etag', this.#computeEtag(body));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
this.#writeHead();
|
|
1015
|
+
this.#committed = true;
|
|
1016
|
+
this.#serverResponse.end(body);
|
|
1017
|
+
return this;
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
this.#handleCommitError(this.#normalizeError(error));
|
|
1020
|
+
return this;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Writes the status line and headers to the underlying server response.
|
|
1026
|
+
*
|
|
1027
|
+
* HTTP/1.1 and HTTP/2 expose `writeHead` with incompatible TypeScript
|
|
1028
|
+
* signatures. Because the internal response can be either one, this method
|
|
1029
|
+
* narrows the type at runtime before calling `writeHead`, allowing
|
|
1030
|
+
* TypeScript to resolve a single signature per branch without relying on
|
|
1031
|
+
* a type assertion.
|
|
1032
|
+
*
|
|
1033
|
+
* The default charset is applied here, on the plain header object, so that
|
|
1034
|
+
* the locked `Content-Type` in `ResponseHeaders` is not mutated.
|
|
1035
|
+
*/
|
|
1036
|
+
#writeHead() {
|
|
1037
|
+
const response = this.#serverResponse;
|
|
1038
|
+
const statusCode = this.#statusCode;
|
|
1039
|
+
|
|
1040
|
+
/** @type {import('node:http').OutgoingHttpHeaders} */
|
|
1041
|
+
const headers = Object.create(null);
|
|
1042
|
+
const plainHeaders = this.#headers.toObject();
|
|
1043
|
+
|
|
1044
|
+
for (const [name, value] of Object.entries(plainHeaders)) {
|
|
1045
|
+
headers[name] = Array.isArray(value) ? value.map(String) : value;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (this.#defaultCharset && typeof headers['content-type'] === 'string') {
|
|
1049
|
+
headers['content-type'] = this.#applyDefaultCharset(headers['content-type']);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if ('stream' in response) {
|
|
1053
|
+
// Http2ServerResponse.
|
|
1054
|
+
response.writeHead(statusCode, headers);
|
|
1055
|
+
} else {
|
|
1056
|
+
// HttpServerResponse.
|
|
1057
|
+
response.writeHead(statusCode, headers);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Appends the configured default charset to a `Content-Type` value when
|
|
1063
|
+
* the media type is eligible and no charset parameter is already present.
|
|
1064
|
+
*
|
|
1065
|
+
* @param {string} contentType - The current `Content-Type` value.
|
|
1066
|
+
* @returns {string} The normalized `Content-Type` value.
|
|
1067
|
+
*/
|
|
1068
|
+
#applyDefaultCharset(contentType) {
|
|
1069
|
+
if (CHARSET_PARAMETER_PATTERN.test(contentType)) {
|
|
1070
|
+
return contentType;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const [mediaType] = contentType.split(';', 1);
|
|
1074
|
+
const normalized = mediaType.trim().toLowerCase();
|
|
1075
|
+
|
|
1076
|
+
const applies =
|
|
1077
|
+
normalized.startsWith('text/') ||
|
|
1078
|
+
CHARSET_ELIGIBLE_MEDIA_TYPES.has(normalized) ||
|
|
1079
|
+
normalized.endsWith('+json') ||
|
|
1080
|
+
normalized.endsWith('+xml');
|
|
1081
|
+
|
|
1082
|
+
if (!applies) {
|
|
1083
|
+
return contentType;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return `${contentType.trim()}; charset=${this.#defaultCharset}`;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Computes a weak ETag for a buffered body.
|
|
1091
|
+
*
|
|
1092
|
+
* @param {string|Uint8Array} body - The buffered body.
|
|
1093
|
+
* @returns {string} The weak ETag value.
|
|
1094
|
+
*/
|
|
1095
|
+
#computeEtag(body) {
|
|
1096
|
+
const buffer = body instanceof Uint8Array ? body : Buffer.from(body);
|
|
1097
|
+
const hash = createHash('sha256').update(buffer).digest('base64url');
|
|
1098
|
+
return `W/"${hash}"`;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Normalizes an unknown thrown value into an `Error` instance.
|
|
1103
|
+
*
|
|
1104
|
+
* JavaScript allows throwing any value. TypeScript types `catch` clauses
|
|
1105
|
+
* as `unknown`. This helper preserves existing `Error` instances and wraps
|
|
1106
|
+
* everything else in a new `Error` so downstream code always receives an
|
|
1107
|
+
* `Error`.
|
|
1108
|
+
*
|
|
1109
|
+
* @param {unknown} error - The value thrown or rejected.
|
|
1110
|
+
* @returns {Error} The normalized error instance.
|
|
1111
|
+
*/
|
|
1112
|
+
#normalizeError(error) {
|
|
1113
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Serializes a value as JSON.
|
|
1118
|
+
*
|
|
1119
|
+
* @param {*} data - The value to serialize.
|
|
1120
|
+
* @returns {string} The serialized JSON string.
|
|
1121
|
+
* @throws {ResponseError} If the value cannot be serialized as JSON.
|
|
1122
|
+
*/
|
|
1123
|
+
#serializeJson(data) {
|
|
1124
|
+
const body = this.#stringify(data);
|
|
1125
|
+
|
|
1126
|
+
if (body === undefined) {
|
|
1127
|
+
throw new ResponseError('INVALID_JSON', {
|
|
1128
|
+
values: { reason: 'value is not serializable to JSON' },
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return body;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Serializes a value using the native JSON serializer.
|
|
1137
|
+
*
|
|
1138
|
+
* @param {*} data - The value to serialize.
|
|
1139
|
+
* @returns {string|undefined} The serialized JSON string, or `undefined` when the value is not serializable as a JSON value.
|
|
1140
|
+
* @throws {ResponseError} If JSON serialization fails.
|
|
1141
|
+
*/
|
|
1142
|
+
#stringify(data) {
|
|
1143
|
+
try {
|
|
1144
|
+
return JSON.stringify(data);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
throw new ResponseError('INVALID_JSON', {
|
|
1147
|
+
cause: this.#normalizeError(error),
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Validates that a value is a string.
|
|
1154
|
+
*
|
|
1155
|
+
* @param {unknown} content - The value to validate.
|
|
1156
|
+
* @throws {ResponseError} If the value is not a string.
|
|
1157
|
+
*/
|
|
1158
|
+
#assertStringBody(content) {
|
|
1159
|
+
if (typeof content !== 'string') {
|
|
1160
|
+
throw new ResponseError('INVALID_BODY', {
|
|
1161
|
+
values: {
|
|
1162
|
+
expected: 'string',
|
|
1163
|
+
received: content === null ? 'null' : typeof content,
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Validates that a value is a supported buffered body.
|
|
1171
|
+
*
|
|
1172
|
+
* Accepted types:
|
|
1173
|
+
* - `string`.
|
|
1174
|
+
* - `Uint8Array` (includes Node.js `Buffer`).
|
|
1175
|
+
*
|
|
1176
|
+
* @param {unknown} body - The response body to validate.
|
|
1177
|
+
* @throws {ResponseError} If the body type is not supported.
|
|
1178
|
+
*/
|
|
1179
|
+
#assertBufferedBody(body) {
|
|
1180
|
+
const isString = typeof body === 'string';
|
|
1181
|
+
const isBuffer = body instanceof Uint8Array;
|
|
1182
|
+
|
|
1183
|
+
if (!isString && !isBuffer) {
|
|
1184
|
+
throw new ResponseError('INVALID_BODY', {
|
|
1185
|
+
values: {
|
|
1186
|
+
received: body === null ? 'null' : typeof body,
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Forwards a commit error to the configured error hook.
|
|
1194
|
+
*
|
|
1195
|
+
* Errors thrown by the error hook itself are intentionally ignored because
|
|
1196
|
+
* the error hook is an observability boundary and must not create another
|
|
1197
|
+
* response failure.
|
|
1198
|
+
*
|
|
1199
|
+
* @param {Error} error - The error produced during response commitment.
|
|
1200
|
+
*/
|
|
1201
|
+
#handleCommitError(error) {
|
|
1202
|
+
if (this.#onCommitError) {
|
|
1203
|
+
try {
|
|
1204
|
+
this.#onCommitError(error);
|
|
1205
|
+
} catch {
|
|
1206
|
+
// The error hook must never interrupt response processing.
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Ensures that the response has not already been committed.
|
|
1213
|
+
*
|
|
1214
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
1215
|
+
*/
|
|
1216
|
+
#assertNotCommitted() {
|
|
1217
|
+
if (this.#committed) {
|
|
1218
|
+
throw new ResponseError('RESPONSE_ALREADY_SENT');
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Ensures that no body has already been described.
|
|
1224
|
+
*
|
|
1225
|
+
* @throws {ResponseError} If a body has already been described.
|
|
1226
|
+
*/
|
|
1227
|
+
#assertBodyNotDescribed() {
|
|
1228
|
+
if (this.#body.kind !== 'none') {
|
|
1229
|
+
throw new ResponseError('BODY_ALREADY_DESCRIBED');
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Ensures that the current status code permits a response body.
|
|
1235
|
+
*
|
|
1236
|
+
* @throws {ResponseError} If the current status code does not allow a body.
|
|
1237
|
+
*/
|
|
1238
|
+
#assertBodyAllowed() {
|
|
1239
|
+
if (EMPTY_BODY_STATUS_CODES.has(this.#statusCode)) {
|
|
1240
|
+
throw new ResponseError('BODY_NOT_ALLOWED', {
|
|
1241
|
+
values: { statusCode: this.#statusCode },
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Restores the response to its initial uncommitted state.
|
|
1248
|
+
*
|
|
1249
|
+
* @returns {void}
|
|
1250
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
1251
|
+
*/
|
|
1252
|
+
#discardUncommittedState() {
|
|
1253
|
+
if (this.#committed) {
|
|
1254
|
+
throw new ResponseError('RESPONSE_ALREADY_SENT');
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
this.#statusCode = DEFAULT_STATUS_CODE;
|
|
1258
|
+
this.#headers = new ResponseHeaders();
|
|
1259
|
+
this.#body = { kind: 'none', value: null };
|
|
1260
|
+
this.#wrap = null;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Prepares the current body to be piped through a Transform stream when
|
|
1265
|
+
* `end()` materializes the response.
|
|
1266
|
+
*
|
|
1267
|
+
* Does not start data flow. Buffer bodies become a `Readable` source;
|
|
1268
|
+
* stream bodies reuse the existing readable as source. The internal body
|
|
1269
|
+
* state becomes `{ kind: 'stream', value: transformStream }` and
|
|
1270
|
+
* `Content-Length` is removed.
|
|
1271
|
+
*
|
|
1272
|
+
* May be called at most once. Only while the response is not committed
|
|
1273
|
+
* and a body has already been described.
|
|
1274
|
+
*
|
|
1275
|
+
* @param {NodeTransform} transformStream - Transform suitable as an intermediate stage of `pipeline()`.
|
|
1276
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
1277
|
+
* @throws {ResponseError} If no body has been described.
|
|
1278
|
+
* @throws {ResponseError} If the body has already been wrapped.
|
|
1279
|
+
* @throws {ResponseError} If `transformStream` is not a usable Transform.
|
|
1280
|
+
*/
|
|
1281
|
+
#wrapBody(transformStream) {
|
|
1282
|
+
this.#assertNotCommitted();
|
|
1283
|
+
|
|
1284
|
+
if (this.#wrap !== null) {
|
|
1285
|
+
throw new ResponseError('BODY_ALREADY_WRAPPED');
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (this.#body.kind === 'none') {
|
|
1289
|
+
throw new ResponseError('BODY_NOT_DESCRIBED');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (
|
|
1293
|
+
transformStream === null ||
|
|
1294
|
+
typeof transformStream !== 'object' ||
|
|
1295
|
+
typeof transformStream.pipe !== 'function' ||
|
|
1296
|
+
typeof transformStream.on !== 'function' ||
|
|
1297
|
+
transformStream.readable !== true ||
|
|
1298
|
+
transformStream.writable !== true
|
|
1299
|
+
) {
|
|
1300
|
+
throw new ResponseError('INVALID_STREAM');
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const source =
|
|
1304
|
+
this.#body.kind === 'buffer'
|
|
1305
|
+
? Readable.from([
|
|
1306
|
+
this.#body.value instanceof Uint8Array
|
|
1307
|
+
? this.#body.value
|
|
1308
|
+
: Buffer.from(this.#body.value),
|
|
1309
|
+
])
|
|
1310
|
+
: this.#body.value;
|
|
1311
|
+
|
|
1312
|
+
this.#wrap = { source, transform: transformStream };
|
|
1313
|
+
this.#body = { kind: 'stream', value: transformStream };
|
|
1314
|
+
this.#headers.remove('content-length');
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Wraps the response body with a Transform stream.
|
|
1319
|
+
*
|
|
1320
|
+
* Runtime-internal entry point. Same pattern as `discardUncommittedState`.
|
|
1321
|
+
* Not part of the public instance API of Response.
|
|
1322
|
+
*
|
|
1323
|
+
* @param {Response} response - Response whose body is wrapped.
|
|
1324
|
+
* @param {NodeTransform} transformStream - Transform stream used as the intermediate pipeline stage.
|
|
1325
|
+
* @throws {TypeError} If `response` is not a `Response` instance.
|
|
1326
|
+
* @throws {ResponseError} If the response cannot be wrapped (see `#wrapBody`).
|
|
1327
|
+
* @example
|
|
1328
|
+
* ```js
|
|
1329
|
+
* import { createGzip } from 'node:zlib';
|
|
1330
|
+
* Response.wrapBody(response, createGzip());
|
|
1331
|
+
* ```
|
|
1332
|
+
*/
|
|
1333
|
+
static wrapBody(response, transformStream) {
|
|
1334
|
+
if (!(response instanceof Response)) {
|
|
1335
|
+
throw new TypeError('Expected a Response instance');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
response.#wrapBody(transformStream);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Discards the uncommitted state of a response, restoring it to its
|
|
1343
|
+
* initial state.
|
|
1344
|
+
*
|
|
1345
|
+
* This operation is runtime-internal. It is intended to be called by the
|
|
1346
|
+
* Kernel when an error occurs before the response has been committed, so
|
|
1347
|
+
* that the system error handler can build a clean response.
|
|
1348
|
+
*
|
|
1349
|
+
* @param {Response} response - Response whose uncommitted state is discarded.
|
|
1350
|
+
* @throws {TypeError} If `response` is not a `Response` instance.
|
|
1351
|
+
* @throws {ResponseError} If the response has already been committed.
|
|
1352
|
+
* @example
|
|
1353
|
+
* ```js
|
|
1354
|
+
* // Kernel error recovery:
|
|
1355
|
+
* Response.discardUncommittedState(exchange.response);
|
|
1356
|
+
* await systemError(error, exchange);
|
|
1357
|
+
* exchange.response.end();
|
|
1358
|
+
* ```
|
|
1359
|
+
*/
|
|
1360
|
+
static discardUncommittedState(response) {
|
|
1361
|
+
if (!(response instanceof Response)) {
|
|
1362
|
+
throw new TypeError('Expected a Response instance');
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
response.#discardUncommittedState();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1370
|
+
export default Response;
|