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.
- package/lib/HttpResponse.js +17 -15
- package/package.json +1 -1
- package/scripts/check-teapot.mjs +40 -0
- package/templates/starter.js +3 -1
- package/test/lib/HttpResponse/async-iterable-middleware.js +37 -0
- package/test/lib/HttpResponse/async-iterable.js +31 -0
- package/test/templates/error-teapot.js +47 -0
- package/test/templates/starter.js +3 -3
package/lib/HttpResponse.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('stream').Writable} Writable */
|
|
7
7
|
|
|
8
|
-
import { PassThrough,
|
|
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, (
|
|
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
|
|
166
|
-
const process = this.finalizers[
|
|
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, (
|
|
188
|
+
pipeline(array, (error) => {
|
|
189
189
|
this.#pipelineComplete = true;
|
|
190
190
|
let nextCallback;
|
|
191
191
|
while ((nextCallback = this.#pipelineCallbacks.shift()) != null) {
|
|
192
|
-
nextCallback(
|
|
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
|
|
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((
|
|
238
|
-
if (
|
|
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
|
|
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
|
|
283
|
-
for (
|
|
284
|
-
const process = this.finalizers[
|
|
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 (
|
|
305
|
-
const process = pendingProcessors[
|
|
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
|
@@ -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
|
+
});
|
package/templates/starter.js
CHANGED
|
@@ -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 (
|
|
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
|
-
//
|
|
86
|
-
|
|
87
|
-
t.is(handler.errorHandlers.
|
|
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
|
|