webhoster 0.3.0 → 0.3.2

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.
@@ -5,7 +5,7 @@
5
5
 
6
6
  /** @typedef {import('stream').Writable} Writable */
7
7
 
8
- import { PassThrough, Readable, pipeline } from 'node:stream';
8
+ import { PassThrough, pipeline } from 'node:stream';
9
9
 
10
10
  import { isWritable } from '../utils/stream.js';
11
11
 
@@ -124,7 +124,7 @@ export default class HttpResponse {
124
124
  async sendRaw(body) {
125
125
  this.body = body;
126
126
  await new Promise((resolve, reject) => {
127
- this.stream.end(body, (err) => (err ? reject(err) : resolve()));
127
+ this.stream.end(body, (error) => (error ? reject(error) : resolve()));
128
128
  });
129
129
  return 0;
130
130
  }
@@ -162,8 +162,8 @@ export default class HttpResponse {
162
162
  // Called directly by user and needs finalizer calls
163
163
 
164
164
  this.isStreaming = true;
165
- for (let i = 0; i < this.finalizers.length; i++) {
166
- const process = this.finalizers[i];
165
+ for (let index = 0; index < this.finalizers.length; index++) {
166
+ const process = this.finalizers[index];
167
167
  const result = process(this);
168
168
  if (result === false) {
169
169
  break;
@@ -185,11 +185,11 @@ export default class HttpResponse {
185
185
 
186
186
  this.#pipeline = array[0];
187
187
  // @ts-ignore Bad typings
188
- pipeline(array, (err) => {
188
+ pipeline(array, (error) => {
189
189
  this.#pipelineComplete = true;
190
190
  let nextCallback;
191
191
  while ((nextCallback = this.#pipelineCallbacks.shift()) != null) {
192
- nextCallback(err);
192
+ nextCallback(error);
193
193
  }
194
194
  });
195
195
  return this.#pipeline;
@@ -209,7 +209,8 @@ export default class HttpResponse {
209
209
  if (body !== undefined) {
210
210
  this.body = body;
211
211
  }
212
- if (this.body instanceof Readable) {
212
+ if (typeof this.body === 'object' && this.body !== null
213
+ && (Symbol.asyncIterator in this.body)) {
213
214
  this.isStreaming = true;
214
215
  this.pipes.push(this.body);
215
216
  }
@@ -234,8 +235,8 @@ export default class HttpResponse {
234
235
  if (!isWritable(this.stream)) return 0;
235
236
  if (this.isStreaming) {
236
237
  await new Promise((resolve, reject) => {
237
- this.getPipeline((err) => {
238
- if (err) reject(err);
238
+ this.getPipeline((error) => {
239
+ if (error) reject(error);
239
240
  resolve();
240
241
  });
241
242
  });
@@ -268,7 +269,8 @@ export default class HttpResponse {
268
269
  this.body = body;
269
270
  }
270
271
 
271
- if (this.body instanceof Readable) {
272
+ if (typeof this.body === 'object' && this.body !== null
273
+ && (Symbol.asyncIterator in this.body)) {
272
274
  this.isStreaming = true;
273
275
  this.pipes.push(this.body);
274
276
  }
@@ -279,9 +281,9 @@ export default class HttpResponse {
279
281
  let needsAsync = false;
280
282
  /** @type {void|Promise<boolean|void>} */
281
283
  let pendingPromise;
282
- let i;
283
- for (i = 0; i < this.finalizers.length; i++) {
284
- const process = this.finalizers[i];
284
+ let index;
285
+ for (index = 0; index < this.finalizers.length; index++) {
286
+ const process = this.finalizers[index];
285
287
  if (needsAsync) {
286
288
  pendingProcessors.push(process);
287
289
  continue;
@@ -301,8 +303,8 @@ export default class HttpResponse {
301
303
  if (pendingPromise) {
302
304
  pendingPromise.then(async (initialResult) => {
303
305
  if (initialResult !== false) {
304
- for (i = 0; i < pendingProcessors.length; i++) {
305
- const process = pendingProcessors[i];
306
+ for (index = 0; index < pendingProcessors.length; index++) {
307
+ const process = pendingProcessors[index];
306
308
  const result = process(this);
307
309
  if (result === true || result == null) {
308
310
  continue;
package/package.json CHANGED
@@ -50,5 +50,5 @@
50
50
  "posttestall": "c8 report"
51
51
  },
52
52
  "type": "module",
53
- "version": "0.3.0"
53
+ "version": "0.3.2"
54
54
  }
@@ -0,0 +1,40 @@
1
+ import http from 'node:http';
2
+ import * as starter from '../templates/starter.js';
3
+
4
+ async function run() {
5
+ const throwingMiddleware = [() => { throw new Error('brew failed'); }];
6
+ const teapotHandler = {
7
+ onError(transaction) {
8
+ transaction.response.status = 418;
9
+ return "I'm a teapot";
10
+ },
11
+ };
12
+
13
+ const listener = await starter.start({ middleware: throwingMiddleware, errorHandlers: [teapotHandler], host: '127.0.0.1', port: 0 });
14
+ try {
15
+ const addr = listener.httpServer.address();
16
+ const port = typeof addr === 'object' ? addr.port : addr;
17
+ const result = await new Promise((resolve, reject) => {
18
+ const req = http.get({ port, path: '/' }, (res) => {
19
+ let data = '';
20
+ res.setEncoding('utf8');
21
+ res.on('data', (c) => { data += c; });
22
+ res.on('end', () => resolve({ status: res.statusCode, body: data }));
23
+ });
24
+ req.on('error', reject);
25
+ });
26
+
27
+ // Print a concise confirmation
28
+ // (CI will show this output in the terminal)
29
+ // eslint-disable-next-line no-console
30
+ console.log('TEAPOT_CHECK_RESULT', JSON.stringify(result));
31
+ } finally {
32
+ await listener.stopHttpServer();
33
+ }
34
+ }
35
+
36
+ run().catch((err) => {
37
+ // eslint-disable-next-line no-console
38
+ console.error('check-teapot failed', err);
39
+ process.exit(1);
40
+ });
@@ -32,7 +32,9 @@ export async function start(options) {
32
32
  // Push by reference to allow post modification
33
33
  HttpHandler.defaultInstance.middleware.push(options.middleware);
34
34
  }
35
- if (!options.errorHandlers) {
35
+ if (options.errorHandlers) {
36
+ HttpHandler.defaultInstance.errorHandlers.push(...options.errorHandlers);
37
+ } else {
36
38
  HttpHandler.defaultInstance.errorHandlers.push(
37
39
  {
38
40
  onError() {
@@ -0,0 +1,37 @@
1
+ import test from 'ava';
2
+ import { PassThrough } from 'node:stream';
3
+
4
+ import HttpHandler from '../../../lib/HttpHandler.js';
5
+ import HttpResponse from '../../../lib/HttpResponse.js';
6
+ import HttpTransaction from '../../../lib/HttpTransaction.js';
7
+
8
+ test('middleware can return async iterable body', async (t) => {
9
+ const stream = new PassThrough();
10
+ const reader = new PassThrough();
11
+ stream.pipe(reader);
12
+
13
+ const response = new HttpResponse({ stream, onSendHeaders: () => {} });
14
+ const transaction = new HttpTransaction({ request: {}, response, socket: {}, httpVersion: '1.1' });
15
+
16
+ const handler = new HttpHandler();
17
+
18
+ // Middleware returns an async generator (async iterable)
19
+ const mw = async () => {
20
+ async function* gen() {
21
+ yield Buffer.from('m1-');
22
+ await new Promise((r) => setTimeout(r, 0));
23
+ yield Buffer.from('m2');
24
+ }
25
+ return gen();
26
+ };
27
+
28
+ const result = await handler.processMiddleware(transaction, mw);
29
+ t.truthy(result !== undefined);
30
+
31
+ let out = '';
32
+ for await (const chunk of reader) {
33
+ out += chunk.toString();
34
+ }
35
+
36
+ t.is(out, 'm1-m2');
37
+ });
@@ -0,0 +1,31 @@
1
+ import test from 'ava';
2
+ import { PassThrough } from 'node:stream';
3
+
4
+ import HttpHandler from '../../../lib/HttpHandler.js';
5
+ import HttpResponse from '../../../lib/HttpResponse.js';
6
+
7
+ test('HttpResponse.send() accepts async iterable body', async (t) => {
8
+ const stream = new PassThrough();
9
+ const reader = new PassThrough();
10
+ stream.pipe(reader);
11
+
12
+ const response = new HttpResponse({ stream, onSendHeaders: () => {} });
13
+
14
+ async function* gen() {
15
+ yield Buffer.from('chunk1-');
16
+ // allow microtask scheduling
17
+ await new Promise((r) => setTimeout(r, 0));
18
+ yield Buffer.from('chunk2');
19
+ }
20
+
21
+ response.body = gen();
22
+
23
+ const result = await response.send();
24
+ t.is(result, HttpHandler.END);
25
+
26
+ let out = '';
27
+ for await (const chunk of reader) {
28
+ out += chunk.toString();
29
+ }
30
+ t.is(out, 'chunk1-chunk2');
31
+ });
@@ -0,0 +1,47 @@
1
+ import test from 'ava';
2
+ import http from 'node:http';
3
+ import HttpListener from '../../helpers/HttpListener.js';
4
+ import HttpHandler from '../../lib/HttpHandler.js';
5
+ import * as starter from '../../templates/starter.js';
6
+
7
+ test.serial('custom error handler returns 418 I\'m a teapot', async (t) => {
8
+ const handler = HttpHandler.defaultInstance;
9
+ const listener = HttpListener.defaultInstance;
10
+
11
+ const mwLen = handler.middleware.length;
12
+ const ehLen = handler.errorHandlers.length;
13
+
14
+ const throwingMiddleware = [() => { throw new Error('brew failed'); }];
15
+ const teapotHandler = {
16
+ onError(transaction) {
17
+ // set custom status and body
18
+ transaction.response.status = 418;
19
+ return "I'm a teapot";
20
+ },
21
+ };
22
+
23
+ await starter.start({ middleware: throwingMiddleware, errorHandlers: [teapotHandler], host: '127.0.0.1', port: 0 });
24
+
25
+ t.truthy(listener.httpServer, 'server started');
26
+ const addr = listener.httpServer.address();
27
+ t.truthy(addr && addr.port, 'server bound');
28
+
29
+ const result = await new Promise((resolve, reject) => {
30
+ const req = http.get({ port: addr.port, path: '/' }, (res) => {
31
+ let data = '';
32
+ res.setEncoding('utf8');
33
+ res.on('data', (chunk) => { data += chunk; });
34
+ res.on('end', () => resolve({ status: res.statusCode, body: data }));
35
+ });
36
+ req.on('error', reject);
37
+ });
38
+
39
+ t.is(result.status, 418);
40
+ t.true(result.body.includes("teapot"));
41
+
42
+ await listener.stopHttpServer();
43
+
44
+ // restore global handler state
45
+ handler.middleware.splice(mwLen);
46
+ handler.errorHandlers.splice(ehLen);
47
+ });
@@ -82,9 +82,9 @@ test.serial('starter.start respects provided errorHandlers and pushes middleware
82
82
  const lastMw = handler.middleware.at(-1);
83
83
  t.is(lastMw, customMiddleware);
84
84
 
85
- // The implementation does not automatically append provided `errorHandlers`;
86
- // it only avoids adding the default when `options.errorHandlers` is supplied.
87
- t.is(handler.errorHandlers.length, ehLength);
85
+ // Provided errorHandlers are appended to the handler.
86
+ t.is(handler.errorHandlers.length, ehLength + customHandlers.length);
87
+ t.is(handler.errorHandlers.at(-1), customHandlers.at(-1));
88
88
 
89
89
  await listener.stopHttpServer();
90
90