webhoster 0.3.2 → 0.3.4
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/.github/workflows/publish.yml +29 -0
- package/.test/index.js +1 -1
- package/README.md +4 -4
- package/data/CookieObject.js +1 -1
- package/{types/index.js → data/custom-types.js} +1 -1
- package/{types/typings.d.ts → data/middleware.d.ts} +1 -1
- package/helpers/HttpListener.js +20 -94
- package/helpers/RequestReader.js +1 -12
- package/helpers/ResponseHeaders.js +9 -10
- package/lib/HttpHandler.js +17 -7
- package/lib/HttpRequest.js +7 -4
- package/lib/HttpResponse.js +11 -5
- package/lib/HttpTransaction.js +2 -2
- package/middleware/AutoHeadersMiddleware.js +2 -2
- package/middleware/CORSMiddleware.js +3 -3
- package/middleware/CaseInsensitiveHeadersMiddleware.js +2 -2
- package/middleware/ContentDecoderMiddleware.js +12 -8
- package/middleware/ContentEncoderMiddleware.js +6 -5
- package/middleware/ContentLengthMiddleware.js +2 -2
- package/middleware/HashMiddleware.js +2 -2
- package/middleware/HeadMethodMiddleware.js +3 -3
- package/middleware/MethodMiddleware.js +3 -3
- package/middleware/PathMiddleware.js +4 -4
- package/middleware/ReadFormData.js +1 -1
- package/middleware/SendJsonMiddleware.js +4 -4
- package/middleware/SendStringMiddleware.js +3 -3
- package/package.json +20 -3
- package/templates/starter.js +2 -2
- package/tsconfig.json +18 -1
- package/utils/headers.js +1 -1
- package/errata/index.js +0 -1
- package/index.js +0 -4
- package/lib/index.js +0 -3
- package/middleware/ContentReaderMiddleware.js +0 -249
- package/middleware/ContentWriterMiddleware.js +0 -161
- package/middleware/SendHeadersMiddleware.js +0 -47
- package/middleware/index.js +0 -11
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # Required for OIDC
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '20'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
|
|
23
|
+
# Ensure npm 11.5.1 or later is installed
|
|
24
|
+
- name: Update npm
|
|
25
|
+
run: npm install -g npm@latest
|
|
26
|
+
- run: npm ci
|
|
27
|
+
- run: npm run build --if-present
|
|
28
|
+
- run: npm test
|
|
29
|
+
- run: npm publish
|
package/.test/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
import * as tls from './tls.js';
|
|
27
27
|
import { ServerResponse } from 'node:http';
|
|
28
28
|
|
|
29
|
-
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
|
|
29
|
+
/** @typedef {import('../data/custom-types.js').MiddlewareFunction} MiddlewareFunction */
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Redirect to HTTPS/2
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ For now, take a look at [/test/index.js](/test/index.js)
|
|
|
39
39
|
* `.defaultInstance` - (`HttpHandler`) - Returns a instance of `HttpHandler` that can be accessed staticly.
|
|
40
40
|
* `.middleware` - (`Middleware[]`) - An array of middleware operations to iterate through when handling a request. It is recommended to create isolated branches (eg: `/images/`; `/api/`; `/views/`; etc.).
|
|
41
41
|
* `.errorHandlers` - (`MiddlewareErrorHandler[]`) - An array of `MiddlewareErrorHandler` that will handle errors and respond appropriately (eg: `res.status = 500`)
|
|
42
|
-
* `.handleRequest` - (`function(
|
|
42
|
+
* `.handleRequest` - (`function(HttpTransaction):Promise<HttpResponse>`) - handles logic for calling middleware and error handlers. Unlikely to be used directly.
|
|
43
43
|
* `.handleHttp1Request` - (`function(IncomingMessage, ServerResponse):Promise<HttpResponse>`) - constructs a new `HttpRequest` and `HttpResponse` based on the HTTP1 parameters and passes it to `handleRequest`
|
|
44
44
|
* `.handleHttp2Stream` - (`function(ServerHttp2Stream, IncomingHttpHeaders, HttpResponseOptions):Promise<HttpResponse>`) - constructs a new `HttpRequest` and `HttpResponse` based on the HTTP2 parameters and passes it to `handleRequest`
|
|
45
45
|
|
|
@@ -141,7 +141,7 @@ async function onGetIndexPage(transaction) {
|
|
|
141
141
|
|
|
142
142
|
Middleware logic flows in a tree structure, allowing for `continue`, `break`, or `end`.
|
|
143
143
|
|
|
144
|
-
A `MiddlewareFunction` is a function that accepts a `
|
|
144
|
+
A `MiddlewareFunction` is a function that accepts a `HttpTransaction` object structured as `{ res: HttpRequest, res: HttpResponse }`. The function can return a instruction with the step in the tree-based logic, status code, or content body to be handled. It maybe return any of these instructions with any of the values as a literal, a `Promise`, or `PromiseLike`:
|
|
145
145
|
|
|
146
146
|
* `HttpHandler.CONTINUE`: Continues on the current branch to the next middleware, or moves to the next branch if there are no siblings left. `alias: true|void|null|undefined`
|
|
147
147
|
* `HttpHandler.BREAK`: Breaks from the current middleware branch, and continues to the next branch. `alias: false`
|
|
@@ -151,7 +151,7 @@ A `MiddlewareFunction` is a function that accepts a `MiddlewareFunctionParams` o
|
|
|
151
151
|
* `Array`: Explicitly passed to `HttpResponse.end()`. This is to support sending an `Array` object instead having it becoming an inline middleware branch.
|
|
152
152
|
* `any`: Any other value returned would automatically be passed to `HttpResponse.end()` which, in turn, uses it's own content handlers (eg: `JSON`; `Readable`), and finally terminates the middleware tree.
|
|
153
153
|
|
|
154
|
-
A `MiddlewareFilter` is a function that accepts a `
|
|
154
|
+
A `MiddlewareFilter` is a function that accepts a `HttpTransaction` and returns a `boolean` or `Promise<boolean>` signaling whether to continue in the branch. `true` translates to `HttpHandler.CONTINUE`. `false` translates to `HttpHandler.BREAK`. There is no support for `HttpHandler.END` logic in a MiddlewareFilter by design.
|
|
155
155
|
|
|
156
156
|
A `MiddlewareErrorHandler` is an `Object` with a `onError` property. `onError` is like a MiddlewareFunction, but includes an `err` item in its parameter object. When the handler is in an error state, it will bubble upwards while searching for the next `MiddlewareErrorHandler`.
|
|
157
157
|
|
|
@@ -163,7 +163,7 @@ To support branching, `Middleware` can also be a `Iterable<Middleware>` (eg: `Se
|
|
|
163
163
|
|
|
164
164
|
### Response Middleware
|
|
165
165
|
* [AuthHeaders](./middleware/AutoHeadersMiddleware.js) - Automatically sends response headers before writing or ending a response stream
|
|
166
|
-
* [ContentLength](./middleware/
|
|
166
|
+
* [ContentLength](./middleware/ContentLengthMiddleware.js) - Sets `Content-Length` based on response stream content writes
|
|
167
167
|
* [Hash](./middleware/HashMiddleware.js) - Sets `ETag`, `Digest`, and `Content-MD5` response headers automatically
|
|
168
168
|
* [ContentEncoder](./middleware/ContentEncoderMiddleware.js) - Applies `Content-Encoding` to response based on `Accept-Encoding` request header
|
|
169
169
|
* [SendJson](./middleware/SendJsonMiddleware.js) - Adds response content processor that encodes objects and arrays to JSON string. Sets `application/json;charset=utf-8`, if content-type not set.
|
package/data/CookieObject.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
/** @typedef {import('node:stream').Duplex} Duplex */
|
|
18
18
|
|
|
19
|
-
/** @typedef {import('./
|
|
19
|
+
/** @typedef {import('./middleware.js').Middleware} Middleware */
|
|
20
20
|
|
|
21
21
|
/** @typedef {true|false|0} MiddlewareFlowInstruction */
|
|
22
22
|
|
package/helpers/HttpListener.js
CHANGED
|
@@ -148,6 +148,7 @@ export default class HttpListener {
|
|
|
148
148
|
|
|
149
149
|
this.httpServer.keepAliveTimeout = 5000;
|
|
150
150
|
this.httpServer.requestTimeout = 300_000;
|
|
151
|
+
// @ts-expect-error Missing types
|
|
151
152
|
this.httpServer.setTimeout(120_000, (socket) => {
|
|
152
153
|
if (!socket) {
|
|
153
154
|
console.warn('HTTP socket (unknown) timed out.');
|
|
@@ -163,6 +164,7 @@ export default class HttpListener {
|
|
|
163
164
|
// console.error('HTTP server error', err);
|
|
164
165
|
});
|
|
165
166
|
this.httpServer.addListener('clientError', this.#httpClientErrorListener = (error, socket) => {
|
|
167
|
+
// @ts-expect-error Missing types
|
|
166
168
|
if (error?.code === 'ECONNRESET') {
|
|
167
169
|
// console.warn('HTTP client connection reset.');
|
|
168
170
|
return;
|
|
@@ -194,6 +196,7 @@ export default class HttpListener {
|
|
|
194
196
|
|
|
195
197
|
this.httpsServer.keepAliveTimeout = 5000;
|
|
196
198
|
this.httpsServer.requestTimeout = 300_000;
|
|
199
|
+
// @ts-expect-error Missing types
|
|
197
200
|
this.httpsServer.setTimeout(120_000, (socket) => {
|
|
198
201
|
if (!socket) {
|
|
199
202
|
// console.warn('HTTPS socket (unknown) timed out.');
|
|
@@ -208,6 +211,7 @@ export default class HttpListener {
|
|
|
208
211
|
// console.error('HTTPS server error', err);
|
|
209
212
|
});
|
|
210
213
|
this.httpsServer.addListener('clientError', this.#httpsClientErrorListener = (error, socket) => {
|
|
214
|
+
// @ts-expect-error Missing types
|
|
211
215
|
if (error?.code === 'ECONNRESET') {
|
|
212
216
|
console.warn('HTTPS client connection reset.');
|
|
213
217
|
return;
|
|
@@ -237,77 +241,7 @@ export default class HttpListener {
|
|
|
237
241
|
host: this.secureHost,
|
|
238
242
|
}, () => {
|
|
239
243
|
this.http2Server.removeListener('error', reject);
|
|
240
|
-
|
|
241
|
-
/** @type {Set<WeakRef<Http2Session>>} */
|
|
242
|
-
const sessions = new Set();
|
|
243
|
-
/** @type {WeakMap<Http2Session, {timestamp:number, identity:string}>} */
|
|
244
|
-
const sessionMetadata = new WeakMap();
|
|
245
|
-
/** @type {Set<WeakRef<Http2Stream>>} */
|
|
246
|
-
const streams = new Set();
|
|
247
|
-
/** @type {WeakMap<Http2Stream, {timestamp:number, identity:string, path:string}>} */
|
|
248
|
-
const streamMetadata = new WeakMap();
|
|
249
|
-
/** @return {void} */
|
|
250
|
-
function logUsage() {
|
|
251
|
-
if (global.gc) {
|
|
252
|
-
console.debug('Perfoming garbage collection.');
|
|
253
|
-
global.gc();
|
|
254
|
-
}
|
|
255
|
-
for (const reference of sessions) {
|
|
256
|
-
const session = reference.deref();
|
|
257
|
-
if (!session) {
|
|
258
|
-
sessions.delete(reference);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
const metadata = sessionMetadata.get(session);
|
|
262
|
-
if (session.destroyed) {
|
|
263
|
-
console.warn('SESSION destroyed from', metadata.identity, 'since', metadata.timestamp);
|
|
264
|
-
} else {
|
|
265
|
-
console.debug('SESSION alive from', metadata.identity, 'since', metadata.timestamp);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
for (const reference of streams) {
|
|
269
|
-
const stream = reference.deref();
|
|
270
|
-
if (!stream) {
|
|
271
|
-
streams.delete(reference);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
const metadata = streamMetadata.get(stream);
|
|
275
|
-
if (stream.destroyed) {
|
|
276
|
-
console.warn('STREAM destroyed from', metadata.identity, 'since', metadata.timestamp, 'for', metadata.path);
|
|
277
|
-
} else {
|
|
278
|
-
console.debug('STREAM alive from', metadata.identity, 'since', metadata.timestamp, 'for', metadata.path);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (sessions.size) {
|
|
282
|
-
console.debug('Active sessions:', sessions.size);
|
|
283
|
-
}
|
|
284
|
-
if (streams.size) {
|
|
285
|
-
console.debug('Active streams:', streams.size);
|
|
286
|
-
}
|
|
287
|
-
if ('process' in globalThis) {
|
|
288
|
-
if ('getActiveResourcesInfo' in globalThis.process) {
|
|
289
|
-
console.dir('Active Resources', globalThis.process.getActiveResourcesInfo());
|
|
290
|
-
}
|
|
291
|
-
if ('_getActiveRequests' in globalThis.process) {
|
|
292
|
-
console.dir('Active Requests', globalThis.process._getActiveRequests());
|
|
293
|
-
}
|
|
294
|
-
if ('_getActiveHandles' in globalThis.process) {
|
|
295
|
-
const handles = globalThis.process._getActiveHandles();
|
|
296
|
-
for (const handle of handles) {
|
|
297
|
-
if (handle instanceof Socket) {
|
|
298
|
-
console.debug('Active Handle (Socket)', `${handle.localAddress}:${handle.localPort} <=> ${handle.remoteAddress}:${handle.remotePort}`);
|
|
299
|
-
} else if (handle instanceof Server) {
|
|
300
|
-
console.debug('Active Handle (Server)', `${handle.address()?.port}`);
|
|
301
|
-
} else {
|
|
302
|
-
console.debug('Active Handle (unknown)', handle);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// setInterval(logUsage, 300_000);
|
|
309
|
-
// logUsage();
|
|
310
|
-
|
|
244
|
+
// @ts-expect-error Missing types
|
|
311
245
|
this.http2Server.setTimeout(120_000, (socket) => {
|
|
312
246
|
if (!socket) {
|
|
313
247
|
console.warn('HTTP2 socket (unknown) timed out.');
|
|
@@ -325,6 +259,7 @@ export default class HttpListener {
|
|
|
325
259
|
this.http2Server.addListener('clientError', this.#http2ClientErrorListener = (error, socket) => {
|
|
326
260
|
// No clear way to signal back to client why a socket has failed because
|
|
327
261
|
// it's unsure if it's HTTP1 or HTTP2. Just destroy and log server-side.
|
|
262
|
+
// @ts-expect-error Missing types
|
|
328
263
|
if (error?.code === 'ECONNRESET') {
|
|
329
264
|
// console.warn('HTTP/2 client connection reset.');
|
|
330
265
|
} else {
|
|
@@ -333,6 +268,7 @@ export default class HttpListener {
|
|
|
333
268
|
if (!socket.destroyed) socket.destroy(error);
|
|
334
269
|
});
|
|
335
270
|
this.http2Server.addListener('sessionError', this.#http2SessionErrorListener = (error, session) => {
|
|
271
|
+
// @ts-expect-error Missing types
|
|
336
272
|
if (error?.code === 'ECONNRESET') {
|
|
337
273
|
// console.warn('HTTP/2 client connection reset.');
|
|
338
274
|
} else if (error?.message === 'SOCKET_TIMEOUT') {
|
|
@@ -344,12 +280,7 @@ export default class HttpListener {
|
|
|
344
280
|
});
|
|
345
281
|
|
|
346
282
|
this.http2Server.addListener('session', this.#http2SessionListener = (session) => {
|
|
347
|
-
sessions.add(new WeakRef(session));
|
|
348
283
|
const identity = `${session.socket.remoteFamily}:${session.socket.remoteAddress}:${session.socket.remotePort}`;
|
|
349
|
-
sessionMetadata.set(session, {
|
|
350
|
-
timestamp: performance.now(),
|
|
351
|
-
identity,
|
|
352
|
-
});
|
|
353
284
|
session.setTimeout(60_000, () => {
|
|
354
285
|
// console.warn(`HTTP/2 session ${identity} timed out.`);
|
|
355
286
|
session.destroy(new Error('SESSION_TIMEOUT'));
|
|
@@ -361,6 +292,7 @@ export default class HttpListener {
|
|
|
361
292
|
session.ping((error) => {
|
|
362
293
|
if (!error) return;
|
|
363
294
|
if (session.destroyed) return;
|
|
295
|
+
// @ts-expect-error Missing types
|
|
364
296
|
if (error.code === 'ERR_HTTP2_PING_CANCEL') return;
|
|
365
297
|
console.error(`Ping to ${identity} failed.`, error);
|
|
366
298
|
});
|
|
@@ -373,49 +305,42 @@ export default class HttpListener {
|
|
|
373
305
|
|
|
374
306
|
// Logic handlers
|
|
375
307
|
this.http2Server.addListener('stream', this.#http2StreamListener = (stream, headers) => {
|
|
376
|
-
streams.add(new WeakRef(stream));
|
|
377
|
-
streamMetadata.set(stream, {
|
|
378
|
-
timestamp: performance.now(),
|
|
379
|
-
identity: `${stream.session?.socket.remoteFamily}:${stream.session?.socket.remoteAddress}:${stream.session?.socket.remotePort}`,
|
|
380
|
-
path: headers[':path'],
|
|
381
|
-
});
|
|
382
|
-
|
|
383
308
|
stream.setTimeout(300_000, () => {
|
|
384
309
|
stream.destroy(new Error('SOCKET_TIMEOUT'));
|
|
385
310
|
});
|
|
386
311
|
stream.addListener('error', (error) => {
|
|
312
|
+
// @ts-expect-error Missing types
|
|
387
313
|
if (error?.code === 'ECONNRESET') {
|
|
388
314
|
// console.warn('HTTP/2 stream connection reset.', headers[':path']);
|
|
389
315
|
} else {
|
|
390
316
|
console.error('HTTP/2 stream error', headers, error);
|
|
391
317
|
}
|
|
392
318
|
});
|
|
319
|
+
// @ts-expect-error Missing types
|
|
393
320
|
this.httpHandler.handleHttp2Stream(stream, headers).catch((error) => {
|
|
394
321
|
console.error('HTTP2 handler failed.', error);
|
|
395
322
|
});
|
|
396
323
|
});
|
|
397
|
-
this.http2Server.addListener('request', this.#http2RequestListener = (request,
|
|
324
|
+
this.http2Server.addListener('request', this.#http2RequestListener = (request, response) => {
|
|
398
325
|
if (request.httpVersionMajor >= 2) return;
|
|
399
|
-
// @ts-
|
|
326
|
+
// @ts-expect-error Missing types
|
|
400
327
|
request.setTimeout(300_000, (socket) => {
|
|
401
328
|
if (!socket) {
|
|
402
329
|
// console.warn('HTTP1 in HTTP2 request (unknown) timed out.');
|
|
403
330
|
return;
|
|
404
331
|
}
|
|
405
|
-
// const identity = `${socket.remoteFamily}:${socket.remoteAddress}:${socket.remotePort}`;
|
|
406
|
-
// console.warn(`HTTP1 in HTTP2 request ${identity} timed out.`);
|
|
407
332
|
socket.destroy(new Error('SOCKET_TIMEOUT'));
|
|
408
333
|
});
|
|
409
|
-
|
|
334
|
+
// @ts-expect-error Missing types
|
|
335
|
+
response.setTimeout(300_000, (socket) => {
|
|
410
336
|
if (!socket) {
|
|
411
337
|
// console.warn('HTTP1 in HTTP2 response (unknown) timed out.');
|
|
412
338
|
return;
|
|
413
339
|
}
|
|
414
|
-
const identity = `${socket.remoteFamily}:${socket.remoteAddress}:${socket.remotePort}`;
|
|
415
|
-
// console.warn(`HTTP1 in HTTP2 response ${identity} timed out.`);
|
|
416
340
|
socket.destroy(new Error('SOCKET_TIMEOUT'));
|
|
417
341
|
});
|
|
418
342
|
request.addListener('error', (error) => {
|
|
343
|
+
// @ts-expect-error Missing types
|
|
419
344
|
if (error?.code === 'ECONNRESET') {
|
|
420
345
|
// console.warn('Request stream connection reset.', req.url);
|
|
421
346
|
} else {
|
|
@@ -425,17 +350,18 @@ export default class HttpListener {
|
|
|
425
350
|
request.destroy(error);
|
|
426
351
|
}
|
|
427
352
|
});
|
|
428
|
-
|
|
353
|
+
response.addListener('error', (error) => {
|
|
354
|
+
// @ts-expect-error Missing types
|
|
429
355
|
if (error?.code === 'ECONNRESET') {
|
|
430
356
|
// console.warn('Response stream connection reset.', req.url);
|
|
431
357
|
} else {
|
|
432
358
|
console.error('Response stream error', request.url, request.headers, error);
|
|
433
359
|
}
|
|
434
|
-
if (!
|
|
435
|
-
|
|
360
|
+
if (!response.destroyed) {
|
|
361
|
+
response.destroy(error);
|
|
436
362
|
}
|
|
437
363
|
});
|
|
438
|
-
this.httpHandler.handleHttp1Request(request,
|
|
364
|
+
this.httpHandler.handleHttp1Request(request, response).catch((error) => {
|
|
439
365
|
console.error('HTTP1 in HTTP2 handler failed.', error);
|
|
440
366
|
});
|
|
441
367
|
});
|
package/helpers/RequestReader.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/** @typedef {import('../types').HttpRequest} HttpRequest */
|
|
1
|
+
/** @typedef {import('../data/custom-types.js').HttpRequest} HttpRequest */
|
|
2
2
|
|
|
3
3
|
import { TextDecoder } from 'node:util';
|
|
4
4
|
import AsyncObject from '../utils/AsyncObject.js';
|
|
5
|
-
import { noop } from '../utils/function.js';
|
|
6
5
|
import RequestHeaders from './RequestHeaders.js';
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -11,7 +10,6 @@ import RequestHeaders from './RequestHeaders.js';
|
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
const BUFFER_SIZE = 4096;
|
|
14
|
-
const STREAM_WAIT_MS = 0;
|
|
15
13
|
|
|
16
14
|
/** @type {WeakMap<HttpRequest, RequestReader>} */
|
|
17
15
|
const cache = new WeakMap();
|
|
@@ -45,8 +43,6 @@ export default class RequestReader {
|
|
|
45
43
|
const hp = new RequestHeaders(this.request);
|
|
46
44
|
let data = Buffer.allocUnsafe(Math.min(BUFFER_SIZE, hp.contentLength || BUFFER_SIZE));
|
|
47
45
|
let bytesWritten = 0;
|
|
48
|
-
/** @type {NodeJS.Timeout} */
|
|
49
|
-
let sendPingTimeout = null;
|
|
50
46
|
this.request.stream.on('readable', () => {
|
|
51
47
|
let chunk;
|
|
52
48
|
// eslint-disable-next-line no-cond-assign
|
|
@@ -70,15 +66,8 @@ export default class RequestReader {
|
|
|
70
66
|
}
|
|
71
67
|
bytesWritten += buffer.copy(data, bytesWritten);
|
|
72
68
|
}
|
|
73
|
-
clearTimeout(sendPingTimeout);
|
|
74
|
-
if (this.request.canPing) {
|
|
75
|
-
sendPingTimeout = setTimeout(() => {
|
|
76
|
-
this.request.ping().catch(noop);
|
|
77
|
-
}, STREAM_WAIT_MS);
|
|
78
|
-
}
|
|
79
69
|
});
|
|
80
70
|
this.request.stream.on('end', () => {
|
|
81
|
-
clearTimeout(sendPingTimeout);
|
|
82
71
|
if (data.length > bytesWritten) {
|
|
83
72
|
// Must partition to clear unsafe allocation
|
|
84
73
|
data = data.subarray(0, bytesWritten);
|
|
@@ -3,7 +3,7 @@ import CookieObject from '../data/CookieObject.js';
|
|
|
3
3
|
import HeadersHandler from './HeadersParser.js';
|
|
4
4
|
|
|
5
5
|
/** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
|
|
6
|
-
/** @typedef {import('../types').CookieDetails} CookieDetails */
|
|
6
|
+
/** @typedef {import('../data/custom-types.js').CookieDetails} CookieDetails */
|
|
7
7
|
|
|
8
8
|
/** @type {(keyof CookieDetails)[]} */
|
|
9
9
|
const COOKIE_DETAIL_KEYS = [
|
|
@@ -22,18 +22,17 @@ const COOKIE_DETAIL_KEYS = [
|
|
|
22
22
|
const instanceCache = new WeakMap();
|
|
23
23
|
|
|
24
24
|
export default class ResponseHeaders extends HeadersHandler {
|
|
25
|
-
/** @param {HttpResponse}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const instance = instanceCache.get(res);
|
|
25
|
+
/** @param {HttpResponse} response */
|
|
26
|
+
constructor(response) {
|
|
27
|
+
const instance = instanceCache.get(response);
|
|
29
28
|
if (instance) return instance;
|
|
30
|
-
super(
|
|
31
|
-
instanceCache.set(
|
|
29
|
+
super(response.headers);
|
|
30
|
+
instanceCache.set(response, this);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
/** @param {HttpResponse}
|
|
35
|
-
static cookies(
|
|
36
|
-
const instance = new ResponseHeaders(
|
|
33
|
+
/** @param {HttpResponse} response */
|
|
34
|
+
static cookies(response) {
|
|
35
|
+
const instance = new ResponseHeaders(response);
|
|
37
36
|
return instance.cookies;
|
|
38
37
|
}
|
|
39
38
|
|
package/lib/HttpHandler.js
CHANGED
|
@@ -7,10 +7,10 @@ import HttpRequest from './HttpRequest.js';
|
|
|
7
7
|
import HttpResponse from './HttpResponse.js';
|
|
8
8
|
import HttpTransaction from './HttpTransaction.js';
|
|
9
9
|
|
|
10
|
-
/** @typedef {import('../types').Middleware} Middleware */
|
|
11
|
-
/** @typedef {import('../types').MiddlewareErrorHandler} MiddlewareErrorHandler */
|
|
12
|
-
/** @typedef {import('../types').MiddlewareFlowInstruction} MiddlewareFlowInstruction */
|
|
13
|
-
/** @typedef {import('../types').RequestMethod} RequestMethod */
|
|
10
|
+
/** @typedef {import('../data/custom-types.js').Middleware} Middleware */
|
|
11
|
+
/** @typedef {import('../data/custom-types.js').MiddlewareErrorHandler} MiddlewareErrorHandler */
|
|
12
|
+
/** @typedef {import('../data/custom-types.js').MiddlewareFlowInstruction} MiddlewareFlowInstruction */
|
|
13
|
+
/** @typedef {import('../data/custom-types.js').RequestMethod} RequestMethod */
|
|
14
14
|
|
|
15
15
|
/** @type {HttpHandler} */
|
|
16
16
|
let defaultInstance = null;
|
|
@@ -145,6 +145,7 @@ export default class HttpHandler {
|
|
|
145
145
|
|
|
146
146
|
// Check if error handler
|
|
147
147
|
const isErrorHandler = (typeof middleware === 'object'
|
|
148
|
+
&& 'onError' in middleware
|
|
148
149
|
&& typeof middleware.onError === 'function');
|
|
149
150
|
|
|
150
151
|
let value = middleware;
|
|
@@ -186,6 +187,7 @@ export default class HttpHandler {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
// Sync operation returned Promise
|
|
190
|
+
// @ts-expect-error TS improperly thinks result can be null
|
|
189
191
|
if (typeof result === 'object' && typeof result.then === 'function') {
|
|
190
192
|
if (isErrorHandler) transaction.error = null;
|
|
191
193
|
result = await result;
|
|
@@ -216,6 +218,7 @@ export default class HttpHandler {
|
|
|
216
218
|
const { treeIndex } = transaction.state;
|
|
217
219
|
treeIndex.push(-1);
|
|
218
220
|
const { length } = value;
|
|
221
|
+
// eslint-disable-next-line no-plusplus
|
|
219
222
|
for (let index = 0; index < length; index++) {
|
|
220
223
|
const innerMiddleware = value[index];
|
|
221
224
|
treeIndex[treeIndex.length - 1] += 1;
|
|
@@ -333,6 +336,7 @@ export default class HttpHandler {
|
|
|
333
336
|
|
|
334
337
|
const request = new HttpRequest({
|
|
335
338
|
headers,
|
|
339
|
+
// @ts-expect-error JSDoc syntax limitation
|
|
336
340
|
method,
|
|
337
341
|
stream: incomingMessage,
|
|
338
342
|
|
|
@@ -428,7 +432,7 @@ export default class HttpHandler {
|
|
|
428
432
|
}
|
|
429
433
|
}
|
|
430
434
|
|
|
431
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
|
432
436
|
const context = this;
|
|
433
437
|
|
|
434
438
|
/** @type {Promise<any>[]} */
|
|
@@ -436,6 +440,7 @@ export default class HttpHandler {
|
|
|
436
440
|
|
|
437
441
|
const request = new HttpRequest({
|
|
438
442
|
headers,
|
|
443
|
+
// @ts-expect-error JSDoc syntax limitation
|
|
439
444
|
method,
|
|
440
445
|
stream,
|
|
441
446
|
|
|
@@ -492,7 +497,7 @@ export default class HttpHandler {
|
|
|
492
497
|
'cache-control',
|
|
493
498
|
]) {
|
|
494
499
|
if (passedHeader in headers) {
|
|
495
|
-
// @ts-
|
|
500
|
+
// @ts-expect-error Coerce
|
|
496
501
|
newHeaders[passedHeader] = headers[passedHeader];
|
|
497
502
|
}
|
|
498
503
|
}
|
|
@@ -500,10 +505,15 @@ export default class HttpHandler {
|
|
|
500
505
|
// Build promise function
|
|
501
506
|
const promiseFunction = async () => {
|
|
502
507
|
try {
|
|
508
|
+
/** @type {import('node:http2').ServerHttp2Stream} */
|
|
503
509
|
const pushStream = await new Promise((resolve, reject) => {
|
|
504
|
-
stream.pushStream(
|
|
510
|
+
stream.pushStream(
|
|
511
|
+
newHeaders,
|
|
512
|
+
((error, newStream) => (error ? reject(error) : resolve(newStream))),
|
|
513
|
+
);
|
|
505
514
|
});
|
|
506
515
|
pushStream.addListener('error', (error) => {
|
|
516
|
+
// @ts-expect-error Missing types
|
|
507
517
|
if (error?.code === 'ECONNRESET') {
|
|
508
518
|
console.warn('HTTP/2 stream connection reset.', headers[':path']);
|
|
509
519
|
} else {
|
package/lib/HttpRequest.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import { URLSearchParams } from 'node:url';
|
|
3
3
|
|
|
4
|
-
/** @typedef {import('../types').RequestMethod} RequestMethod */
|
|
4
|
+
/** @typedef {import('../data/custom-types.js').RequestMethod} RequestMethod */
|
|
5
5
|
/** @typedef {import('http').IncomingHttpHeaders} IncomingHttpHeaders */
|
|
6
|
-
/** @typedef {import('../types
|
|
6
|
+
/** @typedef {import('../data/custom-types.js').MediaType} MediaType */
|
|
7
7
|
|
|
8
8
|
/** @typedef {Partial<MediaType> & {parse:(this:HttpRequest)=>any|PromiseLike<any>, test?:(this:HttpRequest, mediaType: MediaType)=>boolean}} ContentReaderRegistration */
|
|
9
9
|
|
|
@@ -140,7 +140,9 @@ export default class HttpRequest {
|
|
|
140
140
|
if (this.#readable === undefined) {
|
|
141
141
|
if (this.method === 'GET' || this.method === 'HEAD') {
|
|
142
142
|
this.#readable = null;
|
|
143
|
+
// @ts-expect-error Missing types
|
|
143
144
|
} else if (Readable.toWeb) {
|
|
145
|
+
// @ts-expect-error Missing types
|
|
144
146
|
this.#readable = Readable.toWeb(this.stream);
|
|
145
147
|
this.#bodyUsed = true;
|
|
146
148
|
} else {
|
|
@@ -226,7 +228,7 @@ export default class HttpRequest {
|
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
/**
|
|
229
|
-
* @return {Promise<
|
|
231
|
+
* @return {Promise<ArrayBufferLike>}
|
|
230
232
|
*/
|
|
231
233
|
async arrayBuffer() {
|
|
232
234
|
try {
|
|
@@ -264,7 +266,7 @@ export default class HttpRequest {
|
|
|
264
266
|
try {
|
|
265
267
|
const module = await import('node:buffer');
|
|
266
268
|
if ('Blob' in module === false) throw new Error('NOT_SUPPORTED');
|
|
267
|
-
BlobClass = module.Blob;
|
|
269
|
+
BlobClass = /** @type {typeof Blob} */ (module.Blob);
|
|
268
270
|
} catch {
|
|
269
271
|
BlobClass = null;
|
|
270
272
|
}
|
|
@@ -389,6 +391,7 @@ export default class HttpRequest {
|
|
|
389
391
|
}
|
|
390
392
|
}
|
|
391
393
|
|
|
394
|
+
// eslint-disable-next-line class-methods-use-this
|
|
392
395
|
async formData() {
|
|
393
396
|
throw new Error('UNSUPPORTED_MEDIA_TYPE');
|
|
394
397
|
}
|
package/lib/HttpResponse.js
CHANGED
|
@@ -11,9 +11,9 @@ import { isWritable } from '../utils/stream.js';
|
|
|
11
11
|
|
|
12
12
|
/** @typedef {import('http').OutgoingHttpHeaders} OutgoingHttpHeaders */
|
|
13
13
|
/** @typedef {import('stream').Stream} Stream */
|
|
14
|
-
/** @typedef {import('../types
|
|
14
|
+
/** @typedef {import('../data/custom-types.js').Middleware} Middleware */
|
|
15
15
|
/** @typedef {import('./HttpRequest.js').default} HttpRequest */
|
|
16
|
-
/** @typedef {import('../types
|
|
16
|
+
/** @typedef {import('../data/custom-types.js').ResponseFinalizer} ResponseFinalizer */
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @typedef {Object} HttpResponseOptions
|
|
@@ -124,6 +124,7 @@ export default class HttpResponse {
|
|
|
124
124
|
async sendRaw(body) {
|
|
125
125
|
this.body = body;
|
|
126
126
|
await new Promise((resolve, reject) => {
|
|
127
|
+
// @ts-expect-error Error is any
|
|
127
128
|
this.stream.end(body, (error) => (error ? reject(error) : resolve()));
|
|
128
129
|
});
|
|
129
130
|
return 0;
|
|
@@ -135,7 +136,7 @@ export default class HttpResponse {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
|
-
* @param {Transform} stream
|
|
139
|
+
* @param {import('stream').Transform} stream
|
|
139
140
|
* @return {this}
|
|
140
141
|
*/
|
|
141
142
|
addUpstream(stream) {
|
|
@@ -162,6 +163,7 @@ export default class HttpResponse {
|
|
|
162
163
|
// Called directly by user and needs finalizer calls
|
|
163
164
|
|
|
164
165
|
this.isStreaming = true;
|
|
166
|
+
// eslint-disable-next-line no-plusplus
|
|
165
167
|
for (let index = 0; index < this.finalizers.length; index++) {
|
|
166
168
|
const process = this.finalizers[index];
|
|
167
169
|
const result = process(this);
|
|
@@ -183,11 +185,13 @@ export default class HttpResponse {
|
|
|
183
185
|
];
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
// eslint-disable-next-line prefer-destructuring
|
|
189
|
+
this.#pipeline = /** @type {Writable} */ (array[0]);
|
|
190
|
+
// @ts-expect-error Bad typings
|
|
188
191
|
pipeline(array, (error) => {
|
|
189
192
|
this.#pipelineComplete = true;
|
|
190
193
|
let nextCallback;
|
|
194
|
+
// eslint-disable-next-line no-cond-assign
|
|
191
195
|
while ((nextCallback = this.#pipelineCallbacks.shift()) != null) {
|
|
192
196
|
nextCallback(error);
|
|
193
197
|
}
|
|
@@ -282,6 +286,7 @@ export default class HttpResponse {
|
|
|
282
286
|
/** @type {void|Promise<boolean|void>} */
|
|
283
287
|
let pendingPromise;
|
|
284
288
|
let index;
|
|
289
|
+
// eslint-disable-next-line no-plusplus
|
|
285
290
|
for (index = 0; index < this.finalizers.length; index++) {
|
|
286
291
|
const process = this.finalizers[index];
|
|
287
292
|
if (needsAsync) {
|
|
@@ -303,6 +308,7 @@ export default class HttpResponse {
|
|
|
303
308
|
if (pendingPromise) {
|
|
304
309
|
pendingPromise.then(async (initialResult) => {
|
|
305
310
|
if (initialResult !== false) {
|
|
311
|
+
// eslint-disable-next-line no-plusplus
|
|
306
312
|
for (index = 0; index < pendingProcessors.length; index++) {
|
|
307
313
|
const process = pendingProcessors[index];
|
|
308
314
|
const result = process(this);
|
package/lib/HttpTransaction.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/** @typedef {import('./HttpResponse.js').default} HttpResponse */
|
|
3
3
|
/** @typedef {import('stream').Stream} Stream */
|
|
4
4
|
|
|
5
|
-
/** @typedef {import('../types
|
|
6
|
-
/** @typedef {import('../types
|
|
5
|
+
/** @typedef {import('../data/custom-types.js').MediaType} MediaType */
|
|
6
|
+
/** @typedef {import('../data/custom-types.js').Middleware} Middleware */
|
|
7
7
|
/** @typedef {import('stream').Writable} Writable */
|
|
8
8
|
|
|
9
9
|
/** @typedef {Partial<MediaType> & {parse:(this:HttpRequest)=>any|PromiseLike<any>, test?:(this:HttpRequest, mediaType: MediaType)=>boolean}} ContentReaderRegistration */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
|
|
2
|
-
/** @typedef {import('../types').ResponseFinalizer} ResponseFinalizer */
|
|
3
|
-
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
|
|
2
|
+
/** @typedef {import('../data/custom-types.js').ResponseFinalizer} ResponseFinalizer */
|
|
3
|
+
/** @typedef {import('../data/custom-types.js').MiddlewareFunction} MiddlewareFunction */
|
|
4
4
|
|
|
5
5
|
import { Transform } from 'node:stream';
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** @typedef {import('../types').IMiddleware} IMiddleware */
|
|
2
|
-
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
|
|
3
|
-
/** @typedef {import('../types').RequestMethod} RequestMethod */
|
|
1
|
+
/** @typedef {import('../data/custom-types.js').IMiddleware} IMiddleware */
|
|
2
|
+
/** @typedef {import('../data/custom-types.js').MiddlewareFunction} MiddlewareFunction */
|
|
3
|
+
/** @typedef {import('../data/custom-types.js').RequestMethod} RequestMethod */
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef CORSMiddlewareOptions
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import CaseInsensitiveObject from '../utils/CaseInsensitiveObject.js';
|
|
2
2
|
|
|
3
|
-
/** @typedef {import('../types').IMiddleware} IMiddleware */
|
|
4
|
-
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
|
|
3
|
+
/** @typedef {import('../data/custom-types.js').IMiddleware} IMiddleware */
|
|
4
|
+
/** @typedef {import('../data/custom-types.js').MiddlewareFunction} MiddlewareFunction */
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {Object} CaseInsensitiveHeadersMiddlewareOptions
|