@johntalton/http-util 4.0.0 → 4.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
package/src/body.js CHANGED
@@ -14,8 +14,8 @@ export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
14
14
  /**
15
15
  * @typedef {Object} BodyOptions
16
16
  * @property {AbortSignal|undefined} [signal]
17
- * @property {number} [byteLimit]
18
- * @property {number} [contentLength]
17
+ * @property {number|undefined} [byteLimit]
18
+ * @property {number|undefined} [contentLength]
19
19
  * @property {ContentType|undefined} [contentType]
20
20
  */
21
21
 
@@ -94,7 +94,8 @@ export function requestBody(stream, options) {
94
94
 
95
95
 
96
96
  const listener = () => {
97
- controller.error(new Error('Abort Signal Timed out'))
97
+ stats.closed = true
98
+ controller.error(new Error('Abort Signal'))
98
99
  }
99
100
 
100
101
  signal?.addEventListener('abort', listener, { once: true })
@@ -146,6 +147,8 @@ export function requestBody(stream, options) {
146
147
 
147
148
  stream.on('close', () => {
148
149
  // console.log('body reader stream close')
150
+ signal?.removeEventListener('abort', listener)
151
+
149
152
  if(!stats.closed) {
150
153
  stats.closed = true
151
154
  controller.close()
package/src/rate-limit.js CHANGED
@@ -1,3 +1,5 @@
1
+ // https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-10.html
2
+
1
3
  export const HTTP_HEADER_RATE_LIMIT = 'RateLimit'
2
4
  export const HTTP_HEADER_RATE_LIMIT_POLICY = 'RateLimit-Policy'
3
5
 
@@ -11,5 +11,5 @@ const { HTTP_STATUS_ACCEPTED } = http2.constants
11
11
  * @param {Metadata} meta
12
12
  */
13
13
  export function sendAccepted(stream, meta) {
14
- send(stream, HTTP_STATUS_ACCEPTED, {}, undefined, undefined, meta)
14
+ send(stream, HTTP_STATUS_ACCEPTED, {}, [], undefined, undefined, meta)
15
15
  }
@@ -11,5 +11,5 @@ const { HTTP_STATUS_CONFLICT } = http2.constants
11
11
  * @param {Metadata} meta
12
12
  */
13
13
  export function sendConflict(stream, meta) {
14
- send(stream, HTTP_STATUS_CONFLICT, {}, undefined, undefined, meta)
14
+ send(stream, HTTP_STATUS_CONFLICT, {}, [], undefined, undefined, meta)
15
15
  }
@@ -23,5 +23,5 @@ export function sendCreated(stream, location, etag, meta) {
23
23
  send(stream, HTTP_STATUS_CREATED, {
24
24
  [HTTP2_HEADER_LOCATION]: location.href,
25
25
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
26
- }, undefined, undefined, meta)
26
+ }, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
27
27
  }
@@ -23,6 +23,7 @@ export const PREFLIGHT_AGE_SECONDS = '500'
23
23
  * @property {Array<TimingsInfo>} performance
24
24
  * @property {string|undefined} servername
25
25
  * @property {string|undefined} origin
26
+ * @property {Array<[ string, string ]>} customHeaders
26
27
  */
27
28
 
28
29
  /**
@@ -13,5 +13,5 @@ const { HTTP_STATUS_INTERNAL_SERVER_ERROR } = http2.constants
13
13
  * @param {Metadata} meta
14
14
  */
15
15
  export function sendError(stream, message, meta) {
16
- send(stream, HTTP_STATUS_INTERNAL_SERVER_ERROR, {}, CONTENT_TYPE_TEXT, message, meta)
16
+ send(stream, HTTP_STATUS_INTERNAL_SERVER_ERROR, {}, [], CONTENT_TYPE_TEXT, message, meta)
17
17
  }
@@ -12,19 +12,25 @@ const {
12
12
  HTTP2_HEADER_STATUS,
13
13
  HTTP2_HEADER_SERVER,
14
14
  HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
15
- HTTP2_HEADER_CONTENT_TYPE
15
+ HTTP2_HEADER_CONTENT_TYPE,
16
+ HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
17
+
18
+ HTTP2_HEADER_ETAG
16
19
  } = http2.constants
17
20
 
18
21
  /**
19
22
  * @param {number} status
20
23
  * @param {string|undefined} contentType
24
+ * @param {Array<string>} exposedHeaders
21
25
  * @param {Metadata} meta
22
26
  * @returns {OutgoingHttpHeaders}
23
27
  */
24
- export function coreHeaders(status, contentType, meta) {
28
+ export function coreHeaders(status, contentType, exposedHeaders, meta) {
29
+ const exposed = [ HTTP2_HEADER_ETAG, HTTP2_HEADER_SERVER, ...exposedHeaders ]
30
+
25
31
  return {
26
32
  [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
27
- // Access-Control-Expose-Headers
33
+ [HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS]: exposed.join(','),
28
34
  // Access-Control-Allow-Credentials // for non-preflight
29
35
  [HTTP2_HEADER_STATUS]: status,
30
36
  [HTTP2_HEADER_CONTENT_TYPE]: contentType,
@@ -42,3 +48,12 @@ export function performanceHeaders(meta) {
42
48
  [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance)
43
49
  }
44
50
  }
51
+
52
+ /**
53
+ * @param {Metadata} meta
54
+ * @returns {OutgoingHttpHeaders}
55
+ */
56
+ export function customHeaders(meta) {
57
+ const m = new Map(meta.customHeaders?.filter(h => h[0].startsWith('X-')))
58
+ return Object.fromEntries(m)
59
+ }
@@ -9,6 +9,7 @@ export * from './no-content.js'
9
9
  export * from './not-acceptable.js'
10
10
  export * from './not-allowed.js'
11
11
  export * from './not-found.js'
12
+ export * from './not-implemented.js'
12
13
  export * from './not-modified.js'
13
14
  export * from './precondition-failed.js'
14
15
  export * from './preflight.js'
@@ -17,5 +18,6 @@ export * from './timeout.js'
17
18
  export * from './too-many-requests.js'
18
19
  export * from './trace.js'
19
20
  export * from './unauthorized.js'
21
+ export * from './unavailable.js'
20
22
  export * from './unprocessable.js'
21
23
  export * from './unsupported-media.js'
@@ -71,5 +71,5 @@ export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl,
71
71
  [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
72
72
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
73
73
  [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
74
- }, CONTENT_TYPE_JSON, encodedData, meta)
74
+ }, [ HTTP2_HEADER_AGE ], CONTENT_TYPE_JSON, encodedData, meta)
75
75
  }
@@ -20,5 +20,5 @@ const { HTTP_STATUS_NO_CONTENT } = http2.constants
20
20
  export function sendNoContent(stream, etag, meta) {
21
21
  send(stream, HTTP_STATUS_NO_CONTENT, {
22
22
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
23
- }, undefined, undefined, meta)
23
+ }, [], undefined, undefined, meta)
24
24
  }
@@ -19,6 +19,7 @@ export function sendNotAcceptable(stream, supportedTypes, meta) {
19
19
  send(stream,
20
20
  HTTP_STATUS_NOT_ACCEPTABLE,
21
21
  {},
22
+ [],
22
23
  has ? CONTENT_TYPE_JSON : undefined,
23
24
  has ? JSON.stringify(supportedTypes) : undefined,
24
25
  meta)
@@ -18,5 +18,5 @@ const { HTTP2_HEADER_ALLOW } = http2.constants
18
18
  export function sendNotAllowed(stream, methods, meta) {
19
19
  send(stream, HTTP_STATUS_METHOD_NOT_ALLOWED, {
20
20
  [HTTP2_HEADER_ALLOW]: methods.join(',')
21
- }, undefined, undefined, meta)
21
+ }, [ HTTP2_HEADER_ALLOW ], undefined, undefined, meta)
22
22
  }
@@ -13,5 +13,5 @@ const { HTTP_STATUS_NOT_FOUND } = http2.constants
13
13
  * @param {Metadata} meta
14
14
  */
15
15
  export function sendNotFound(stream, message, meta) {
16
- send(stream, HTTP_STATUS_NOT_FOUND, {}, CONTENT_TYPE_TEXT, message, meta)
16
+ send(stream, HTTP_STATUS_NOT_FOUND, {}, [], CONTENT_TYPE_TEXT, message, meta)
17
17
  }
@@ -0,0 +1,17 @@
1
+ import http2 from 'node:http2'
2
+ import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
+ import { send } from './send-util.js'
4
+
5
+ /** @import { ServerHttp2Stream } from 'node:http2' */
6
+ /** @import { Metadata } from './defs.js' */
7
+
8
+ const { HTTP_STATUS_NOT_IMPLEMENTED } = http2.constants
9
+
10
+ /**
11
+ * @param {ServerHttp2Stream} stream
12
+ * @param {string|undefined} message
13
+ * @param {Metadata} meta
14
+ */
15
+ export function sendNotImplemented(stream, message, meta) {
16
+ send(stream, HTTP_STATUS_NOT_IMPLEMENTED, {}, [], CONTENT_TYPE_TEXT, message, meta)
17
+ }
@@ -30,5 +30,5 @@ export function sendNotModified(stream, etag, age, cacheControl, meta) {
30
30
  [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
31
31
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
32
32
  [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
33
- }, undefined, undefined, meta)
33
+ }, [ HTTP2_HEADER_AGE ], undefined, undefined, meta)
34
34
  }
@@ -11,5 +11,5 @@ const { HTTP_STATUS_PRECONDITION_FAILED } = http2.constants
11
11
  * @param {Metadata} meta
12
12
  */
13
13
  export function sendPreconditionFailed(stream, meta) {
14
- send(stream, HTTP_STATUS_PRECONDITION_FAILED, {}, undefined, undefined, meta)
14
+ send(stream, HTTP_STATUS_PRECONDITION_FAILED, {}, [], undefined, undefined, meta)
15
15
  }
@@ -35,5 +35,5 @@ export function sendPreflight(stream, methods, meta) {
35
35
  ].join(','),
36
36
  [HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS
37
37
  // Access-Control-Allow-Credentials
38
- }, undefined, undefined, meta)
38
+ }, [], undefined, undefined, meta)
39
39
  }
@@ -7,6 +7,7 @@ import { sendNoContent } from './no-content.js'
7
7
  import { sendNotAcceptable } from './not-acceptable.js'
8
8
  import { sendNotAllowed } from './not-allowed.js'
9
9
  import { sendNotFound } from './not-found.js'
10
+ import { sendNotImplemented } from './not-implemented.js'
10
11
  import { sendNotModified } from './not-modified.js'
11
12
  import { sendPreconditionFailed } from './precondition-failed.js'
12
13
  import { sendPreflight } from './preflight.js'
@@ -15,6 +16,7 @@ import { sendTimeout } from './timeout.js'
15
16
  import { sendTooManyRequests } from './too-many-requests.js'
16
17
  import { sendTrace } from './trace.js'
17
18
  import { sendUnauthorized } from './unauthorized.js'
19
+ import { sendUnavailable } from './unavailable.js'
18
20
  import { sendUnprocessable } from './unprocessable.js'
19
21
  import { sendUnsupportedMediaType } from './unsupported-media.js'
20
22
 
@@ -28,6 +30,7 @@ export const Response = {
28
30
  notAcceptable: sendNotAcceptable,
29
31
  notAllowed: sendNotAllowed,
30
32
  notFound: sendNotFound,
33
+ notImplemented: sendNotImplemented,
31
34
  notModified: sendNotModified,
32
35
  preconditionFailed: sendPreconditionFailed,
33
36
  preflight: sendPreflight,
@@ -36,6 +39,7 @@ export const Response = {
36
39
  tooManyRequests: sendTooManyRequests,
37
40
  trace: sendTrace,
38
41
  unauthorized: sendUnauthorized,
42
+ unavailable: sendUnavailable,
39
43
  unprocessable: sendUnprocessable,
40
44
  unsupportedMediaType: sendUnsupportedMediaType
41
45
  }
@@ -1,4 +1,8 @@
1
- import { coreHeaders, performanceHeaders } from './header-util.js'
1
+ import {
2
+ coreHeaders,
3
+ customHeaders,
4
+ performanceHeaders
5
+ } from './header-util.js'
2
6
 
3
7
  /** @import { ServerHttp2Stream } from 'node:http2' */
4
8
  /** @import { IncomingHttpHeaders } from 'node:http2' */
@@ -8,24 +12,29 @@ import { coreHeaders, performanceHeaders } from './header-util.js'
8
12
  * @param {ServerHttp2Stream} stream
9
13
  * @param {number} status
10
14
  * @param {IncomingHttpHeaders} headers
15
+ * @param {Array<string>} exposedHeaders
11
16
  * @param {string|undefined} contentType
12
17
  * @param {ArrayBufferLike|ArrayBufferView|string|undefined} body
13
18
  * @param {Metadata} meta
14
19
  */
15
- export function send(stream, status, headers, contentType, body, meta) {
20
+ export function send(stream, status, headers, exposedHeaders, contentType, body, meta) {
16
21
  // if(status >= 400) { console.warn(status, body) }
17
22
  if(status === 401) { console.warn(status, body) }
18
23
  if(status === 404) { console.warn(status, body) }
19
- if(status === 500) { console.warn(status, body) }
24
+ if(status >= 500) { console.warn(status, body) }
20
25
 
21
26
  if(stream === undefined) { return }
22
27
  if(stream.closed) { return }
23
28
 
24
29
  if(!stream.headersSent) {
30
+ const custom = customHeaders(meta)
31
+ const exposed = [ ...exposedHeaders, ...Object.keys(custom) ]
32
+
25
33
  stream.respond({
26
- ...coreHeaders(status, contentType, meta),
34
+ ...coreHeaders(status, contentType, exposed, meta),
27
35
  ...performanceHeaders(meta),
28
- ...headers
36
+ ...headers,
37
+ ...custom
29
38
  })
30
39
  }
31
40
 
@@ -23,7 +23,7 @@ export function sendSSE(stream, meta) {
23
23
  const status = activeStream ? HTTP_STATUS_OK : SSE_INACTIVE_STATUS_CODE
24
24
 
25
25
  stream.respond({
26
- ...coreHeaders(status, SSE_MIME, meta),
26
+ ...coreHeaders(status, SSE_MIME, [], meta),
27
27
  ...performanceHeaders(meta)
28
28
 
29
29
  // [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS]: 'true'
@@ -17,5 +17,5 @@ const { HTTP2_HEADER_CONNECTION } = http2.constants
17
17
  export function sendTimeout(stream, meta) {
18
18
  send(stream, HTTP_STATUS_REQUEST_TIMEOUT, {
19
19
  [HTTP2_HEADER_CONNECTION]: 'close'
20
- }, undefined, undefined, meta)
20
+ }, [], undefined, undefined, meta)
21
21
  }
@@ -30,6 +30,11 @@ export function sendTooManyRequests(stream, limitInfo, policies, meta) {
30
30
  [HTTP_HEADER_RATE_LIMIT]: RateLimit.from(limitInfo),
31
31
  [HTTP_HEADER_RATE_LIMIT_POLICY]: RateLimitPolicy.from(...policies)
32
32
  },
33
+ [
34
+ HTTP2_HEADER_RETRY_AFTER,
35
+ HTTP_HEADER_RATE_LIMIT,
36
+ HTTP_HEADER_RATE_LIMIT_POLICY
37
+ ],
33
38
  CONTENT_TYPE_TEXT,
34
39
  `Retry After ${limitInfo.resetSeconds} Seconds`,
35
40
  meta)
@@ -35,5 +35,5 @@ export function sendTrace(stream, method, url, headers, meta) {
35
35
  ]
36
36
  .join('\n')
37
37
 
38
- send(stream, HTTP_STATUS_OK, {}, CONTENT_TYPE_MESSAGE_HTTP, reconstructed, meta)
38
+ send(stream, HTTP_STATUS_OK, {}, [], CONTENT_TYPE_MESSAGE_HTTP, reconstructed, meta)
39
39
  }
@@ -13,5 +13,5 @@ const { HTTP_STATUS_UNAUTHORIZED } = http2.constants
13
13
  export function sendUnauthorized(stream, meta) {
14
14
  send(stream, HTTP_STATUS_UNAUTHORIZED, {
15
15
  // WWW-Authenticate
16
- }, undefined, undefined, meta)
16
+ }, [], undefined, undefined, meta)
17
17
  }
@@ -0,0 +1,24 @@
1
+ import http2 from 'node:http2'
2
+ import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
+ import { send } from './send-util.js'
4
+
5
+ /** @import { ServerHttp2Stream } from 'node:http2' */
6
+ /** @import { Metadata } from './defs.js' */
7
+
8
+ const {
9
+ HTTP2_HEADER_RETRY_AFTER
10
+ } = http2.constants
11
+
12
+ const { HTTP_STATUS_SERVICE_UNAVAILABLE } = http2.constants
13
+
14
+ /**
15
+ * @param {ServerHttp2Stream} stream
16
+ * @param {string|undefined} message
17
+ * @param {number|undefined} retryAfter
18
+ * @param {Metadata} meta
19
+ */
20
+ export function sendUnavailable(stream, message, retryAfter, meta) {
21
+ send(stream, HTTP_STATUS_SERVICE_UNAVAILABLE, {
22
+ [HTTP2_HEADER_RETRY_AFTER]: Number.isInteger(retryAfter) ? `${retryAfter}` : undefined
23
+ }, [ HTTP2_HEADER_RETRY_AFTER ], CONTENT_TYPE_TEXT, message, meta)
24
+ }
@@ -11,5 +11,5 @@ const { HTTP_STATUS_UNPROCESSABLE_ENTITY } = http2.constants
11
11
  * @param {Metadata} meta
12
12
  */
13
13
  export function sendUnprocessable(stream, meta) {
14
- send(stream, HTTP_STATUS_UNPROCESSABLE_ENTITY, {}, undefined, undefined, meta)
14
+ send(stream, HTTP_STATUS_UNPROCESSABLE_ENTITY, {}, [], undefined, undefined, meta)
15
15
  }
@@ -17,5 +17,5 @@ export function sendUnsupportedMediaType(stream, acceptableMediaType, meta) {
17
17
 
18
18
  send(stream, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, {
19
19
  [HTTP_HEADER_ACCEPT_POST]: acceptable.join(',')
20
- }, undefined, undefined, meta)
20
+ }, [ HTTP_HEADER_ACCEPT_POST ], undefined, undefined, meta)
21
21
  }