@johntalton/http-util 3.0.1 → 4.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "3.0.1",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
package/src/body.js CHANGED
@@ -13,9 +13,9 @@ export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
13
13
 
14
14
  /**
15
15
  * @typedef {Object} BodyOptions
16
- * @property {AbortSignal} [signal]
17
- * @property {number} [byteLimit]
18
- * @property {number} [contentLength]
16
+ * @property {AbortSignal|undefined} [signal]
17
+ * @property {number|undefined} [byteLimit]
18
+ * @property {number|undefined} [contentLength]
19
19
  * @property {ContentType|undefined} [contentType]
20
20
  */
21
21
 
@@ -0,0 +1,57 @@
1
+ /** @typedef {'no-cache'|'no-store'|'no-transform'|'must-revalidate'|'immutable'|'must-understand'} Directives */
2
+
3
+ /**
4
+ * @typedef {Object} CacheControlOptions
5
+ * @property {boolean|undefined} [priv]
6
+ * @property {boolean|undefined} [pub]
7
+ * @property {number|undefined} [maxAge]
8
+ * @property {number|undefined} [staleWhileRevalidate]
9
+ * @property {number|undefined} [staleIfError]
10
+ * @property {Directives|Array<Directives>|undefined} [directives]
11
+ */
12
+
13
+ export class CacheControl {
14
+ /**
15
+ * @param {CacheControlOptions} options
16
+ * @returns {string|undefined}
17
+ */
18
+ static encode(options) {
19
+ const {
20
+ pub,
21
+ priv,
22
+ maxAge,
23
+ directives,
24
+ staleWhileRevalidate,
25
+ staleIfError
26
+ } = options
27
+
28
+ const result = []
29
+
30
+ if(pub !== undefined && pub && !priv) { result.push('public') }
31
+ if(priv !== undefined && priv && !pub) { result.push('private') }
32
+
33
+ if(directives !== undefined) {
34
+ result.push(...(Array.isArray(directives) ? directives : [ directives ]))
35
+ }
36
+
37
+ if(maxAge !== undefined && Number.isInteger(maxAge) && maxAge >= 0) {
38
+ result.push(`max-age=${maxAge}`)
39
+ }
40
+
41
+ if(staleWhileRevalidate !== undefined && Number.isInteger(staleWhileRevalidate) && staleWhileRevalidate >= 0) {
42
+ result.push(`stale-while-revalidate=${staleWhileRevalidate}`)
43
+ }
44
+
45
+ if(staleIfError !== undefined && Number.isInteger(staleIfError) && staleIfError >= 0) {
46
+ result.push(`stale-if-error=${staleIfError}`)
47
+ }
48
+
49
+ return result.join(', ')
50
+ }
51
+ }
52
+
53
+ // console.log(CacheControl.encode({
54
+ // priv: true,
55
+ // maxAge: 60,
56
+ // directives: ['must-revalidate', 'no-transform']
57
+ // }))
@@ -31,7 +31,7 @@
31
31
  * @property {number} hour
32
32
  * @property {number} minute
33
33
  * @property {number} second
34
- * @property {Date} date
34
+ * @property {Date|undefined} [date]
35
35
  */
36
36
 
37
37
  export const CONDITION_ETAG_SEPARATOR = ','
@@ -168,6 +168,34 @@ export class Conditional {
168
168
  .filter(item => item !== undefined)
169
169
  }
170
170
 
171
+ /**
172
+ * @param {Array<EtagItem>} etagItemList
173
+ */
174
+ static hasAny(etagItemList) {
175
+ return etagItemList.find(item => item.any) !== undefined
176
+ }
177
+
178
+ /**
179
+ * @param {Array<EtagItem>} etagItemList
180
+ * @param {string} etag
181
+ */
182
+ static hasEtag(etagItemList, etag) {
183
+ return etagItemList.find(item => item.etag === etag) !== undefined
184
+ }
185
+
186
+ /**
187
+ * @param {IMFFixDate|undefined} fixDate
188
+ * @returns {string|undefined}
189
+ */
190
+ static encodeFixDate(fixDate) {
191
+ if(fixDate === undefined) { return undefined }
192
+ if(fixDate.date !== undefined) { return fixDate.date.toUTCString() }
193
+
194
+ const { year, month, day, hour, minute, second } = fixDate
195
+ const d = new Date(Date.UTC(year, DATE_MONTHS.indexOf(month), day, hour, minute, second))
196
+ return d.toUTCString()
197
+ }
198
+
171
199
  /**
172
200
  * @param {String|string|undefined} matchHeader
173
201
  * @returns {IMFFixDate|undefined}
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,18 +12,26 @@ 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,
33
+ [HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS]: exposed.join(','),
34
+ // Access-Control-Allow-Credentials // for non-preflight
27
35
  [HTTP2_HEADER_STATUS]: status,
28
36
  [HTTP2_HEADER_CONTENT_TYPE]: contentType,
29
37
  [HTTP2_HEADER_SERVER]: meta.servername
@@ -40,3 +48,12 @@ export function performanceHeaders(meta) {
40
48
  [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance)
41
49
  }
42
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'
@@ -11,10 +11,12 @@ import {
11
11
  } from '../content-type.js'
12
12
  import { send } from './send-util.js'
13
13
  import { Conditional } from '../conditional.js'
14
+ import { CacheControl } from '../cache-control.js'
14
15
 
15
16
  /** @import { ServerHttp2Stream } from 'node:http2' */
16
17
  /** @import { Metadata } from './defs.js' */
17
18
  /** @import { EtagItem } from '../conditional.js' */
19
+ /** @import { CacheControlOptions } from '../cache-control.js' */
18
20
 
19
21
  /** @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun */
20
22
 
@@ -22,7 +24,8 @@ const {
22
24
  HTTP2_HEADER_CONTENT_ENCODING,
23
25
  HTTP2_HEADER_VARY,
24
26
  HTTP2_HEADER_CACHE_CONTROL,
25
- HTTP2_HEADER_ETAG
27
+ HTTP2_HEADER_ETAG,
28
+ HTTP2_HEADER_AGE
26
29
  } = http2.constants
27
30
 
28
31
  const { HTTP_STATUS_OK } = http2.constants
@@ -40,9 +43,11 @@ export const ENCODER_MAP = new Map([
40
43
  * @param {Object} obj
41
44
  * @param {string|undefined} encoding
42
45
  * @param {EtagItem|undefined} etag
46
+ * @param {number|undefined} age
47
+ * @param {CacheControlOptions} cacheControl
43
48
  * @param {Metadata} meta
44
49
  */
45
- export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
50
+ export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, meta) {
46
51
  if(stream.closed) { return }
47
52
 
48
53
  const json = JSON.stringify(obj)
@@ -63,8 +68,8 @@ export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
63
68
  send(stream, HTTP_STATUS_OK, {
64
69
  [HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
65
70
  [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
66
- [HTTP2_HEADER_CACHE_CONTROL]: 'private',
67
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
68
- // [HTTP2_HEADER_AGE]: age
69
- }, CONTENT_TYPE_JSON, encodedData, meta)
71
+ [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
72
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
73
+ [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
74
+ }, [ HTTP2_HEADER_AGE ], CONTENT_TYPE_JSON, encodedData, meta)
70
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
+ }
@@ -1,10 +1,12 @@
1
1
  import http2 from 'node:http2'
2
2
  import { send } from './send-util.js'
3
3
  import { Conditional } from '../conditional.js'
4
+ import { CacheControl } from '../cache-control.js'
4
5
 
5
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
7
  /** @import { Metadata } from './defs.js' */
7
8
  /** @import { EtagItem } from '../conditional.js' */
9
+ /** @import { CacheControlOptions } from '../cache-control.js' */
8
10
 
9
11
  const {
10
12
  HTTP2_HEADER_AGE,
@@ -19,13 +21,14 @@ const { HTTP_STATUS_NOT_MODIFIED } = http2.constants
19
21
  * @param {ServerHttp2Stream} stream
20
22
  * @param {EtagItem|undefined} etag
21
23
  * @param {number|undefined} age
24
+ * @param {CacheControlOptions} cacheControl
22
25
  * @param {Metadata} meta
23
26
  */
24
- export function sendNotModified(stream, etag, age, meta) {
27
+ export function sendNotModified(stream, etag, age, cacheControl, meta) {
25
28
  send(stream, HTTP_STATUS_NOT_MODIFIED, {
26
29
  [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
27
- [HTTP2_HEADER_CACHE_CONTROL]: 'private',
30
+ [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
28
31
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
29
32
  [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
30
- }, undefined, undefined, meta)
33
+ }, [ HTTP2_HEADER_AGE ], undefined, undefined, meta)
31
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
  }
@@ -11,7 +11,10 @@ import { send } from './send-util.js'
11
11
  const {
12
12
  HTTP2_HEADER_CONTENT_TYPE,
13
13
  HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
14
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
14
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
15
+ HTTP2_HEADER_IF_MATCH,
16
+ HTTP2_HEADER_IF_NONE_MATCH,
17
+ HTTP2_HEADER_AUTHORIZATION
15
18
  } = http2.constants
16
19
 
17
20
  const { HTTP_STATUS_OK } = http2.constants
@@ -24,7 +27,13 @@ const { HTTP_STATUS_OK } = http2.constants
24
27
  export function sendPreflight(stream, methods, meta) {
25
28
  send(stream, HTTP_STATUS_OK, {
26
29
  [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS]: methods.join(','),
27
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: ['Authorization', HTTP2_HEADER_CONTENT_TYPE].join(','),
30
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: [
31
+ HTTP2_HEADER_IF_MATCH,
32
+ HTTP2_HEADER_IF_NONE_MATCH,
33
+ HTTP2_HEADER_AUTHORIZATION,
34
+ HTTP2_HEADER_CONTENT_TYPE
35
+ ].join(','),
28
36
  [HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS
29
- }, undefined, undefined, meta)
37
+ // Access-Control-Allow-Credentials
38
+ }, [], undefined, undefined, meta)
30
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
  }