@superhero/http-server 4.0.0

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/index.test.js ADDED
@@ -0,0 +1,464 @@
1
+ import assert from 'node:assert'
2
+ import path from 'node:path'
3
+ import util from 'node:util'
4
+ import fs from 'node:fs'
5
+ import https from 'node:https'
6
+ import tls from 'node:tls'
7
+ import Request from '@superhero/http-request'
8
+ import Router from '@superhero/router'
9
+ import Locator from '@superhero/locator'
10
+ import HttpServer from '@superhero/http-server'
11
+ import { execSync } from 'node:child_process'
12
+ import { afterEach, beforeEach, suite, test } from 'node:test'
13
+
14
+ util.inspect.defaultOptions.depth = 10
15
+
16
+ suite('@superhero/http-server', () =>
17
+ {
18
+ let locator, server
19
+
20
+ beforeEach(async () =>
21
+ {
22
+ if(beforeEach.skip)
23
+ {
24
+ return
25
+ }
26
+
27
+ locator = new Locator()
28
+
29
+ await locator.eagerload(
30
+ {
31
+ '@superhero/http-server': path.resolve('./index.js')
32
+ })
33
+
34
+ server = locator.locate('@superhero/http-server')
35
+ server.log.info = () => null
36
+ })
37
+
38
+ afterEach (() =>
39
+ {
40
+ if(afterEach.skip)
41
+ {
42
+ return
43
+ }
44
+ locator.clear()
45
+ })
46
+
47
+ suite('Lifecycle', async () =>
48
+ {
49
+ test('Can instantiate HttpServer', () =>
50
+ {
51
+ const router = new Router(locator)
52
+ assert.doesNotThrow(() => new HttpServer(router))
53
+ })
54
+
55
+ test('Can bootstrap server with non-secure settings', async () =>
56
+ {
57
+ await server.bootstrap()
58
+
59
+ assert.ok(server.http1Server)
60
+ assert.ok(server.http1Server.constructor.name === 'Server')
61
+
62
+ assert.ok(server.http2Server)
63
+ assert.ok(server.http2Server.constructor.name === 'Http2Server')
64
+ })
65
+
66
+ test('Listens and closes the server as expected', async () =>
67
+ {
68
+ await server.bootstrap()
69
+ await assert.doesNotReject(server.listen())
70
+ await assert.doesNotReject(server.close())
71
+ })
72
+
73
+ test('Rejects if server is not available to listen error', async () =>
74
+ {
75
+ await assert.rejects(
76
+ server.listen(),
77
+ (error) => error.code === 'E_HTTP_SERVER_NOT_AVAILIBLE')
78
+ })
79
+
80
+ test('Rejects if server is not available to close error', async () =>
81
+ {
82
+ await assert.rejects(
83
+ server.close(),
84
+ (error) => error.code === 'E_HTTP_SERVER_NOT_AVAILIBLE')
85
+ })
86
+ })
87
+
88
+ suite('Routing and Requests', async () =>
89
+ {
90
+ let request
91
+
92
+ beforeEach(async () =>
93
+ {
94
+ await server.bootstrap()
95
+ await server.listen()
96
+ const { port } = server.gateway.address()
97
+ request = new Request(
98
+ {
99
+ base: `http://localhost:${port}`,
100
+ timeout: 1e3,
101
+ doNotThrowOnErrorStatus: true
102
+ })
103
+ })
104
+
105
+ afterEach(async () =>
106
+ {
107
+ await server.close()
108
+ })
109
+
110
+ suite('HTTP/1', () =>
111
+ {
112
+ beforeEach(() => request.config.headers = { 'connection': 'close' })
113
+ sameTestsForBothProtocols()
114
+
115
+ test('Support connection keep-alive header', async () =>
116
+ {
117
+ server.router.setRoutes(
118
+ { 'foo':
119
+ { criteria : '/test/foo',
120
+ dispatcher : 'foo-dispatcher'
121
+ }})
122
+
123
+ locator.set('foo-dispatcher', { dispatch: () => null })
124
+
125
+ // Set the keep-alive timeout to 10 seconds on the server.
126
+ const keepAlive = 10
127
+ server.http1Server.keepAliveTimeout = keepAlive * 1e3
128
+
129
+ // Make a request with the connection header set to keep-alive.
130
+ const response1 = await request.get({ url: `/test/foo`, headers: { 'connection': 'keep-alive' }})
131
+ assert.equal(response1.status, 200, 'Should have received a 200 status')
132
+ assert.equal(response1.headers['connection'], 'keep-alive', 'Should echo the connection header')
133
+ assert.equal(response1.headers['keep-alive'], 'timeout=' + keepAlive, 'Should have a keep-alive header')
134
+
135
+ // Make a request with the connection header set to close.
136
+ const response2 = await request.get({ url: `/test/foo`, headers: { 'connection': 'close' }})
137
+ assert.equal(response2.status, 200, 'Should have received a 200 status')
138
+ assert.equal(response2.headers['connection'], 'close', 'Should echo the connection header')
139
+ assert.equal(response2.headers['keep-alive'], undefined, 'Should not have the keep-alive header')
140
+ })
141
+ })
142
+
143
+ suite('HTTP/2', () =>
144
+ {
145
+ beforeEach(() => request.connect())
146
+ afterEach(() => request.close())
147
+ sameTestsForBothProtocols()
148
+ })
149
+
150
+ function sameTestsForBothProtocols()
151
+ {
152
+ test('Can dispatch a request aligned to the route map', async () =>
153
+ {
154
+ server.router.setRoutes(
155
+ { 'foo':
156
+ { criteria : '/test/foo',
157
+ dispatcher : 'foo-dispatcher'
158
+ },
159
+ 'bar':
160
+ { criteria : '/test/bar',
161
+ dispatcher : 'bar-dispatcher' }})
162
+
163
+ let dispatched
164
+ locator.set('foo-dispatcher', { dispatch: () => dispatched = 'foo' })
165
+ locator.set('bar-dispatcher', { dispatch: () => dispatched = 'bar' })
166
+
167
+ const fooResponse = await request.get(`/test/foo`)
168
+
169
+ assert.equal(fooResponse.status, 200, 'Should have received a 200 status')
170
+ assert.equal(dispatched, 'foo', 'Should have dispatched the foo route')
171
+
172
+ const barResponse = await request.get(`/test/bar`)
173
+ assert.equal(barResponse.status, 200, 'Should have received a 200 status for the bar request')
174
+ assert.equal(dispatched, 'bar', 'Should have dispatched the bar route')
175
+ })
176
+
177
+ test('Can alter the output body', async () =>
178
+ {
179
+ server.router.setRoutes(
180
+ { 'foo':
181
+ { criteria : '/test/foo',
182
+ dispatcher : 'foo-dispatcher' }})
183
+
184
+ locator.set('foo-dispatcher',
185
+ { dispatch: (_, session) => session.view.body.foo = 'bar' })
186
+
187
+ const response = await request.get(`/test/foo`)
188
+ assert.equal(response.status, 200, 'Should have received a 200 status')
189
+ assert.equal(response.body.foo, 'bar', 'Response body should have been altered')
190
+ })
191
+
192
+ test('Can stream HTML5 standard Server-Sent Events (SSE)', async () =>
193
+ {
194
+ server.router.setRoutes(
195
+ { 'sse':
196
+ { criteria : '/test/sse',
197
+ dispatcher : 'sse-dispatcher' }})
198
+
199
+ locator.set('sse-dispatcher',
200
+ { dispatch: (_, session) =>
201
+ {
202
+ session.view.stream.write({ foo: 'bar' })
203
+ session.view.stream.write({ bar: 'baz' })
204
+ session.view.stream.write({ baz: 'qux' })
205
+ session.view.stream.end()
206
+ }})
207
+
208
+ const response = await request.get(`/test/sse`)
209
+
210
+ assert.equal(response.status, 200, 'Should have received a 200 status')
211
+ assert.equal(response.body.length, 3, 'Response body should have three records')
212
+ assert.equal(response.body[0]?.data?.foo, 'bar', 'Response body should have the first record')
213
+ assert.equal(response.body[1]?.data?.bar, 'baz', 'Response body should have the second record')
214
+ assert.equal(response.body[2]?.data?.baz, 'qux', 'Response body should have the third record')
215
+ })
216
+
217
+ test('Can alter the output headers', async () =>
218
+ {
219
+ server.router.setRoutes(
220
+ { 'foo':
221
+ { criteria : '/test/foo',
222
+ dispatcher : 'foo-dispatcher' }})
223
+
224
+ locator.set('foo-dispatcher',
225
+ { dispatch: (_, session) => session.view.headers.foo = 'bar' })
226
+
227
+ const response = await request.get(`/test/foo`)
228
+ assert.equal(response.status, 200, 'Should have received a 200 status')
229
+ assert.equal(response.headers.foo, 'bar', 'Response header should have been altered')
230
+ })
231
+
232
+ test('Can alter the output status', async () =>
233
+ {
234
+ server.router.setRoutes(
235
+ { 'foo':
236
+ { criteria : '/test/foo',
237
+ dispatcher : 'foo-dispatcher' }})
238
+
239
+ locator.set('foo-dispatcher',
240
+ { dispatch: (_, session) => session.view.status = 204 })
241
+
242
+ const response = await request.get(`/test/foo`)
243
+ assert.equal(response.status, 204, 'Should have received a 204 status')
244
+ })
245
+
246
+ test('Can abort the dispatcher', async () =>
247
+ {
248
+ server.router.setRoutes(
249
+ { 'foo':
250
+ { criteria : '/test/foo',
251
+ dispatcher : 'foo-dispatcher' }})
252
+
253
+ locator.set('foo-dispatcher',
254
+ { dispatch: (_, session) => session.abortion.abort(new Error('Aborted')) })
255
+
256
+ const response = await request.get(`/test/foo`)
257
+
258
+ assert.equal(response.status, 500, 'Should have failed with a 500 status')
259
+ assert.equal(response.body.error, 'Aborted', 'Response body should have the abortion message')
260
+ })
261
+
262
+ test('Can describe an abortion in detail', async () =>
263
+ {
264
+ server.router.setRoutes(
265
+ { 'foo':
266
+ { criteria : '/test/foo',
267
+ dispatcher : 'foo-dispatcher' }})
268
+
269
+ locator.set('foo-dispatcher',
270
+ {
271
+ dispatch: (_, session) =>
272
+ {
273
+ const error = new Error('Aborted')
274
+ error.code = 'E_TEST_ABORT'
275
+ error.cause = new Error('Abortion test')
276
+ error.cause.code = 'E_TEST_ABORT_CAUSE'
277
+ error.cause.cause = 'Deeper detailed test'
278
+ session.abortion.abort(error)
279
+ }
280
+ })
281
+
282
+ const response = await request.get(`/test/foo`)
283
+
284
+ assert.equal(response.status, 500, 'Should have failed with a 500 status')
285
+ assert.equal(response.body.error, 'Aborted', 'Response body should have the abortion message')
286
+ assert.equal(response.body.code, 'E_TEST_ABORT', 'Response body should have the abortion code')
287
+ assert.equal(response.body.details?.[0], 'E_TEST_ABORT_CAUSE - Abortion test', 'Response body should have the abortion cause')
288
+ assert.equal(response.body.details?.[1], 'Deeper detailed test', 'Response body should have the deeper detailed error message')
289
+ })
290
+
291
+ test('Can manage thrown errors in the dispatcher', async () =>
292
+ {
293
+ server.router.setRoutes(
294
+ { 'foo':
295
+ { criteria : '/test/foo',
296
+ dispatcher : 'foo-dispatcher' }})
297
+
298
+ locator.set('foo-dispatcher',
299
+ {
300
+ dispatch: () =>
301
+ {
302
+ const error = new Error('Failed dispatcher test')
303
+ error.code = 'E_TEST_FAILED_DISPATCHER'
304
+ throw error
305
+ }
306
+ })
307
+
308
+ let errorLoggerCalled = false
309
+
310
+ server.log.error = (error) =>
311
+ {
312
+ errorLoggerCalled = true
313
+ assert.equal(error.code, 'E_ROUTER_DISPATCH_FAILED', 'Should throw router error')
314
+ assert.equal(error.cause.code, 'E_TEST_FAILED_DISPATCHER', 'The error should have the dispatcher error as cause')
315
+ }
316
+
317
+ const response = await request.get(`/test/foo`)
318
+
319
+ assert.equal(response.status, 500, 'Should have failed with a 500 status')
320
+ assert.equal(response.body.error, 'Failed dispatcher test', 'Response body should have the error message')
321
+ assert.equal(response.body.code, 'E_TEST_FAILED_DISPATCHER', 'Response body should have the error code')
322
+ assert.equal(errorLoggerCalled, true, 'The error logger should have been called')
323
+ })
324
+
325
+ test('Can not mistakenly access the wrong view property', async () =>
326
+ {
327
+ server.router.setRoutes(
328
+ { 'foo':
329
+ { criteria : '/test/foo',
330
+ dispatcher : 'foo-dispatcher' }})
331
+
332
+ locator.set('foo-dispatcher',
333
+ { dispatch: (_, session) => session.view.invalidAttribute })
334
+
335
+ let errorLoggerCalled = false
336
+
337
+ server.log.error = (error) =>
338
+ {
339
+ errorLoggerCalled = true
340
+ assert.equal(error.code, 'E_ROUTER_DISPATCH_FAILED')
341
+ assert.equal(error.cause.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_READABLE')
342
+ }
343
+
344
+ const response = await request.get(`/test/foo`)
345
+
346
+ assert.equal(response.status, 500, 'Should have failed with a 500 status')
347
+ assert.equal(response.body.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_READABLE')
348
+ assert.equal(errorLoggerCalled, true)
349
+ })
350
+
351
+ test('Can not mistakenly assign a value to the wrong view property', async () =>
352
+ {
353
+ server.router.setRoutes(
354
+ { 'foo':
355
+ { criteria : '/test/foo',
356
+ dispatcher : 'foo-dispatcher' }})
357
+
358
+ locator.set('foo-dispatcher',
359
+ { dispatch: (_, session) => session.view.invalidAttribute = 'This should not be possible' })
360
+
361
+
362
+ let errorLoggerCalled = false
363
+
364
+ server.log.error = (error) =>
365
+ {
366
+ errorLoggerCalled = true
367
+ assert.equal(error.code, 'E_ROUTER_DISPATCH_FAILED')
368
+ assert.equal(error.cause.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_WRITABLE')
369
+ }
370
+
371
+ const response = await request.get(`/test/foo`)
372
+
373
+ assert.equal(response.status, 500, 'Should have failed with a 500 status')
374
+ assert.equal(response.body.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_WRITABLE')
375
+ assert.equal(errorLoggerCalled, true)
376
+ })
377
+ }
378
+ })
379
+
380
+ suite('HTTPS server with self-signed certificate', async () =>
381
+ {
382
+ const
383
+ tlsVersions = ['TLSv1.2', 'TLSv1.3'],
384
+ algorithms =
385
+ {
386
+ 'RSA:2048' : `openssl req -newkey rsa:2048 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
387
+ 'RSA:4096' : `openssl req -newkey rsa:4096 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
388
+ 'ECDSA:P-256' : `openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
389
+ 'ECDSA:P-384' : `openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-384 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
390
+ 'ECDSA:P-521' : `openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-521 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
391
+ 'EdDSA:Ed25519' : `openssl req -newkey ed25519 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
392
+ 'EdDSA:Ed448' : `openssl req -newkey ed448 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
393
+ }
394
+
395
+ for(const tlsVersion of tlsVersions)
396
+ {
397
+ suite(tlsVersion, () =>
398
+ {
399
+ for(const algorithm in algorithms)
400
+ {
401
+ test(algorithm, async (sub) =>
402
+ {
403
+ fs.mkdirSync('test', { recursive: true })
404
+ execSync(algorithms[algorithm], { stdio: ['ignore', 'ignore', 'pipe'] })
405
+
406
+ const
407
+ cert = fs.readFileSync('test/server.cert').toString(),
408
+ key = fs.readFileSync('test/private.key').toString()
409
+
410
+ fs.rmSync('test', { recursive: true, force: true })
411
+
412
+ await assert.doesNotReject(server.bootstrap(
413
+ { server: { cert, key, minVersion: tlsVersion, maxVersion: tlsVersion },
414
+ router:
415
+ { routes:
416
+ { 'test':
417
+ { criteria : '/test',
418
+ dispatcher : 'test-dispatcher' }}}}))
419
+
420
+ locator.set('test-dispatcher',
421
+ { dispatch: (_, session) => session.view.body.dispatched = true })
422
+
423
+ assert.ok(server.gateway instanceof tls.Server)
424
+ assert.ok(server.http1Server instanceof https.Server)
425
+ assert.ok(server.http2Server?.constructor.name === 'Http2SecureServer')
426
+
427
+ await assert.doesNotReject(server.listen())
428
+
429
+ const { port } = server.gateway.address()
430
+ const request = new Request({ base: `https://localhost:${port}`, timeout: 5e3, rejectUnauthorized: false })
431
+
432
+ beforeEach.skip = true
433
+ afterEach.skip = true
434
+
435
+ await sub.test('HTTP1', async () =>
436
+ {
437
+ const http1Response = await request.get({ url:'/test', headers: { 'connection': 'close' }})
438
+ assert.equal(http1Response.status, 200, 'Should have received a 200 status')
439
+ assert.equal(http1Response.body.dispatched, true, 'Should have dispatched the test route')
440
+ })
441
+
442
+ await sub.test('HTTP2', async () =>
443
+ {
444
+ await request.connect()
445
+ const http2Response1 = await request.get('/test')
446
+ const http2Response2 = await request.get('/test')
447
+ await request.close()
448
+
449
+ assert.equal(http2Response1.status, 200, 'HTTP2 request 1 should have received a 200 status')
450
+ assert.equal(http2Response2.status, 200, 'HTTP2 request 2 should have received a 200 status')
451
+ assert.equal(http2Response1.body.dispatched, true, 'HTTP2 request 1 should have dispatched the test route')
452
+ assert.equal(http2Response2.body.dispatched, true, 'HTTP2 request 2 should have dispatched the test route')
453
+ })
454
+
455
+ delete beforeEach.skip
456
+ delete afterEach.skip
457
+
458
+ await assert.doesNotReject(server.close())
459
+ })
460
+ }
461
+ })
462
+ }
463
+ })
464
+ })
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @memberof @superhero/http-server:middleware/upstream/header
3
+ */
4
+ export default new class AcceptHeaderUpstreamMiddleware
5
+ {
6
+ #listFormat = new Intl.ListFormat('en', { style:'long', type:'disjunction' })
7
+ #normalize = (route) => route.replace('accept-', '').trim()
8
+
9
+ dispatch(request, session)
10
+ {
11
+ const
12
+ splitHeader = request.headers['accept']?.toLowerCase().split(',') || [],
13
+ accepts = splitHeader.map(this.#normalize),
14
+ routes = Object.keys(session.route).filter((key) => key.startsWith('accept-') && session.route[key]),
15
+ supports = routes.map((route) => [this.#normalize(route), route])
16
+
17
+ for(let accepted of accepts)
18
+ {
19
+ accepted = accepted.split(';')[0].split('*')[0]
20
+
21
+ for(let [supported, route] in supports)
22
+ {
23
+ supported = supported.split('*')[0]
24
+
25
+ if(supported.startsWith(accepted)
26
+ || accepted.startsWith(supported))
27
+ {
28
+ const
29
+ dispatcher = session.route[route],
30
+ dispatchers = Array.isArray(dispatcher) ? dispatcher : [dispatcher],
31
+ uniqueList = dispatchers.filter((item) => false === session.chain.dispatchers.includes(item))
32
+
33
+ // insert the forward routed dispatcher(s) after the current dispatcher in the chain
34
+ // for the dispatcher chain iterator to dispatch it/them next
35
+ session.chain.dispatchers.splice(session.chain.index, 0, ...uniqueList)
36
+ return
37
+ }
38
+ }
39
+ }
40
+
41
+ const
42
+ allowed = supports.map(([ supported ]) => supported),
43
+ error = new Error(`The requested resource "${request.method} ${request.url}" can not be delivered in requested header accept media types: ${this.#listFormat.format(accepts) || 'none are defined'}`)
44
+
45
+ error.code = 'E_HTTP_SERVER_MIDDLEWARE_ACCEPT_HEADER_NO_MATCHING_DISPATCHER'
46
+ error.status = 406
47
+ error.headers = { accept:allowed.join(',') }
48
+ error.cause = `Supported accept header media types are: ${this.#listFormat.format(allowed) || 'none are defined'}`
49
+
50
+ session.abortion.abort(error)
51
+ }
52
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Does not validate headers, just assumes that the body is a JSON string
3
+ *
4
+ * @memberof @superhero/http-server:middleware/upstream/header/content-type/application
5
+ */
6
+ export default new class ContentTypeApplicationJsonHeaderUpstreamMiddleware
7
+ {
8
+ async dispatch(request, session)
9
+ {
10
+ const body = await request.body
11
+
12
+ if(body)
13
+ {
14
+ try
15
+ {
16
+ request.body = JSON.parse(body)
17
+ }
18
+ catch(reason)
19
+ {
20
+ const error = new Error('The body is not a valid JSON string')
21
+ error.code = 'E_HTTP_SERVER_MIDDLEWARE_CONTENT_TYPE_APPLICATION_JSON_INVALID_BODY'
22
+ error.status = 400
23
+ error.cause = 'The buffered body could not be parsed as a JSON string'
24
+
25
+ session.abortion.abort(error)
26
+ }
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @memberof @superhero/http-server:middleware/upstream/header
3
+ */
4
+ export default new class ContentTypeHeaderUpstreamMiddleware
5
+ {
6
+ #listFormat = new Intl.ListFormat('en', { style:'long', type:'disjunction' })
7
+
8
+ dispatch(request, session)
9
+ {
10
+ const
11
+ contentType = request.headers['content-type']?.toLowerCase().split(';')[0].split('*')[0].trim(),
12
+ routes = Object.keys(session.route).filter((key) => key.startsWith('content-type-') && session.route[key]),
13
+ supports = routes.map((route) => [route.replace('content-type-', '').trim(), route])
14
+
15
+ for(let [supported, route] in supports)
16
+ {
17
+ supported = supported.split('*')[0]
18
+
19
+ if(supported.startsWith(contentType)
20
+ || contentType.startsWith(supported))
21
+ {
22
+ const
23
+ dispatcher = session.route[route],
24
+ dispatchers = Array.isArray(dispatcher) ? dispatcher : [dispatcher],
25
+ uniqueList = dispatchers.filter((item) => false === session.chain.dispatchers.includes(item))
26
+
27
+ // insert the forward routed dispatcher(s) after the current dispatcher in the chain
28
+ // for the dispatcher chain iterator to dispatch it/them next
29
+ session.chain.dispatchers.splice(session.chain.index, 0, ...uniqueList)
30
+ return
31
+ }
32
+ }
33
+
34
+ const
35
+ allowed = supports.map(([ supported ]) => supported),
36
+ error = new Error(`The requested resource "${request.method} ${request.url}" does not support content-type "${request.headers['content-type']}"`)
37
+
38
+ error.code = 'E_HTTP_SERVER_MIDDLEWARE_CONTENT_TYPE_NO_MATCHING_DISPATCHER'
39
+ error.status = 415
40
+ error.headers = { accept:allowed.join(',') }
41
+ error.cause = `Supported content-type headers are: ${this.#listFormat.format(allowed) || 'none are defined'}`
42
+
43
+ session.abortion.abort(error)
44
+ }
45
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @memberof @superhero/http-server:middleware/upstream
3
+ */
4
+ export default new class MethodUpstreamMiddleware
5
+ {
6
+ #listFormat = new Intl.ListFormat('en', { style:'long', type:'disjunction' })
7
+
8
+ dispatch(request, session)
9
+ {
10
+ const
11
+ method = request.method.toLowerCase(),
12
+ dispatcher = session.route['method-' + method] || session.route['method-*']
13
+
14
+ if(dispatcher)
15
+ {
16
+ const
17
+ dispatchers = Array.isArray(dispatcher) ? dispatcher : [dispatcher],
18
+ uniqueList = dispatchers.filter((item) => false === session.chain.dispatchers.includes(item))
19
+
20
+ // insert the forward routed dispatcher(s) after the current dispatcher in the chain
21
+ // for the dispatcher chain iterator to dispatch it/them next
22
+ session.chain.dispatchers.splice(session.chain.index, 0, ...uniqueList)
23
+ return
24
+ }
25
+
26
+ const
27
+ supports = Object.keys(session.route).filter((key) => key.startsWith('method-')),
28
+ allowed = supports.map((supported) => supported.replace('method-', '').toUpperCase()).sort(),
29
+ error = new Error(`The requested resource "${request.url}" does not support method "${request.method}"`)
30
+
31
+ error.code = 'E_HTTP_SERVER_MIDDLEWARE_METHOD_NO_MATCHING_DISPATCHER'
32
+ error.status = 405
33
+ error.headers = { allow:allowed.join(',') }
34
+ error.cause = `Supported methods are: ${this.#listFormat.format(allowed) || 'none are defined'}`
35
+
36
+ session.abortion.abort(error)
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@superhero/http-server",
3
+ "version": "4.0.0",
4
+ "description": "HTTP(S) server component supporting both HTTP 1.1 and HTTP 2.0",
5
+ "keywords": [
6
+ "http server",
7
+ "http2 server",
8
+ "http2",
9
+ "http 2.0",
10
+ "http 1.1",
11
+ "https"
12
+ ],
13
+ "main": "index.js",
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "exports": {
17
+ ".": "./index.js",
18
+ "./view": "./view.js",
19
+ "./*/*/*": "./*/*/*.js",
20
+ "./*/*/*/*": "./*/*/*/*.js",
21
+ "./*/*/*/*/*": "./*/*/*/*/*.js",
22
+ "./*/*/*/*/*/*": "./*/*/*/*/*/*.js"
23
+ },
24
+ "dependencies": {
25
+ "@superhero/router": "^4.0.5"
26
+ },
27
+ "devDependencies": {
28
+ "@superhero/locator": "^4.1.1",
29
+ "@superhero/http-request": "^4.0.9"
30
+ },
31
+ "scripts": {
32
+ "test": "node --trace-warnings --test --experimental-test-coverage"
33
+ },
34
+ "author": {
35
+ "name": "Erik Landvall",
36
+ "email": "erik@landvall.se"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/superhero/http-server"
41
+ }
42
+ }