@johntalton/http-util 6.1.0 → 7.0.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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/src/body.js +11 -6
  4. package/src/defs.js +14 -5
  5. package/src/headers/accept-encoding.js +28 -5
  6. package/src/headers/accept-language.js +29 -5
  7. package/src/headers/accept.js +77 -32
  8. package/src/headers/cache-control.js +6 -3
  9. package/src/headers/client-hints.js +4 -3
  10. package/src/headers/conditional.js +18 -18
  11. package/src/headers/content-type.js +1 -1
  12. package/src/headers/link.js +9 -4
  13. package/src/headers/multipart.js +18 -17
  14. package/src/headers/range.js +4 -2
  15. package/src/headers/rate-limit.js +20 -4
  16. package/src/headers/server-timing.js +5 -3
  17. package/src/headers/util/kvp.js +2 -1
  18. package/src/headers/util/mime.js +17 -1
  19. package/src/headers/util/quote.js +1 -1
  20. package/src/headers/util/whitespace.js +1 -1
  21. package/src/headers/www-authenticate.js +35 -11
  22. package/src/response/2xx/accepted.js +2 -2
  23. package/src/response/2xx/bytes.js +2 -31
  24. package/src/response/2xx/created.js +8 -21
  25. package/src/response/2xx/json.js +6 -19
  26. package/src/response/2xx/no-content.js +4 -18
  27. package/src/response/2xx/partial-content.js +1 -28
  28. package/src/response/2xx/preflight.js +18 -25
  29. package/src/response/3xx/found.js +8 -6
  30. package/src/response/3xx/moved-permanently.js +8 -6
  31. package/src/response/3xx/multiple-choices.js +3 -3
  32. package/src/response/3xx/not-modified.js +14 -26
  33. package/src/response/3xx/permanent-redirect.js +7 -5
  34. package/src/response/3xx/see-other.js +8 -6
  35. package/src/response/3xx/temporary-redirect.js +7 -5
  36. package/src/response/4xx/bad-request.js +2 -3
  37. package/src/response/4xx/conflict.js +2 -2
  38. package/src/response/4xx/content-too-large.js +2 -2
  39. package/src/response/4xx/forbidden.js +3 -3
  40. package/src/response/4xx/gone.js +2 -2
  41. package/src/response/4xx/im-a-teapot.js +2 -2
  42. package/src/response/4xx/not-acceptable.js +2 -11
  43. package/src/response/4xx/not-allowed.js +6 -14
  44. package/src/response/4xx/payment-required.js +4 -4
  45. package/src/response/4xx/precondition-failed.js +4 -18
  46. package/src/response/4xx/range-not-satisfiable.js +4 -13
  47. package/src/response/4xx/timeout.js +3 -3
  48. package/src/response/4xx/too-many-requests.js +3 -20
  49. package/src/response/4xx/unauthorized.js +4 -4
  50. package/src/response/4xx/unprocessable.js +2 -2
  51. package/src/response/4xx/unsupported-media.js +16 -23
  52. package/src/response/5xx/error.js +2 -3
  53. package/src/response/5xx/insufficient-storage.js +2 -2
  54. package/src/response/5xx/not-implemented.js +2 -3
  55. package/src/response/5xx/unavailable.js +3 -20
  56. package/src/response/header-util.js +9 -5
  57. package/src/response/response.js +2 -2
  58. package/src/response/send-util.js +102 -30
@@ -1,7 +1,6 @@
1
1
  import http2 from 'node:http2'
2
2
 
3
- import { CONTENT_TYPE_TEXT } from '../../headers/content-type.js'
4
- import { send } from '../send-util.js'
3
+ import { send_error } from '../send-util.js'
5
4
 
6
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
7
6
  /** @import { Metadata } from '../../defs.js' */
@@ -14,5 +13,5 @@ const { HTTP_STATUS_NOT_IMPLEMENTED } = http2.constants
14
13
  * @param {Metadata} meta
15
14
  */
16
15
  export function sendNotImplemented(stream, message, meta) {
17
- send(stream, HTTP_STATUS_NOT_IMPLEMENTED, {}, [], CONTENT_TYPE_TEXT, message, meta)
16
+ send_error(stream, HTTP_STATUS_NOT_IMPLEMENTED, message, undefined, meta)
18
17
  }
@@ -1,37 +1,20 @@
1
1
  import http2 from 'node:http2'
2
2
 
3
- import { CONTENT_TYPE_TEXT } from '../../headers/content-type.js'
4
- import { send } from '../send-util.js'
3
+ import { send_error } from '../send-util.js'
5
4
 
6
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
7
6
  /** @import { SendInfo, Metadata } from '../../defs.js' */
8
7
 
9
- const {
10
- HTTP2_HEADER_RETRY_AFTER
11
- } = http2.constants
12
-
13
8
  const { HTTP_STATUS_SERVICE_UNAVAILABLE } = http2.constants
14
9
 
15
- /**
16
- * @param {ServerHttp2Stream} stream
17
- * @param {string|undefined} message
18
- * @param {number|undefined} retryAfter
19
- * @param {Metadata} meta
20
- */
21
- export function sendUnavailable(stream, message, retryAfter, meta) {
22
- _sendUnavailable(stream, message, { retryAfter }, meta)
23
- }
24
-
25
10
  /**
26
11
  * @param {ServerHttp2Stream} stream
27
12
  * @param {string|undefined} message
28
13
  * @param {Pick<SendInfo, 'retryAfter'>} info
29
14
  * @param {Metadata} meta
30
15
  */
31
- export function _sendUnavailable(stream, message, info, meta) {
16
+ export function sendUnavailable(stream, message, info, meta) {
32
17
  const { retryAfter } = info
33
18
 
34
- send(stream, HTTP_STATUS_SERVICE_UNAVAILABLE, {
35
- [HTTP2_HEADER_RETRY_AFTER]: Number.isInteger(retryAfter) ? `${retryAfter}` : undefined
36
- }, [ HTTP2_HEADER_RETRY_AFTER ], CONTENT_TYPE_TEXT, message, meta)
19
+ send_error(stream, HTTP_STATUS_SERVICE_UNAVAILABLE, message, retryAfter, meta)
37
20
  }
@@ -1,5 +1,7 @@
1
1
  import http2 from 'node:http2'
2
2
 
3
+ import { COMMON_LIST_VALUE_JOINER_COMMA } from '../defs.js'
4
+
3
5
  import {
4
6
  HTTP_HEADER_SERVER_TIMING,
5
7
  HTTP_HEADER_TIMING_ALLOW_ORIGIN,
@@ -15,8 +17,8 @@ const {
15
17
  HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
16
18
  HTTP2_HEADER_CONTENT_TYPE,
17
19
  HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
18
-
19
- HTTP2_HEADER_ETAG
20
+ HTTP2_HEADER_ETAG,
21
+ // HTTP2_HEADER_STRICT_TRANSPORT_SECURITY
20
22
  } = http2.constants
21
23
 
22
24
  /**
@@ -27,11 +29,12 @@ const {
27
29
  * @returns {OutgoingHttpHeaders}
28
30
  */
29
31
  export function coreHeaders(status, contentType, exposedHeaders, meta) {
30
- const exposed = [ HTTP2_HEADER_ETAG, HTTP2_HEADER_SERVER, ...exposedHeaders ]
32
+ const exposed = [ HTTP2_HEADER_ETAG, HTTP2_HEADER_SERVER, ...exposedHeaders ] // todo include lastModified
31
33
 
32
34
  return {
35
+ // todo [HTTP2_HEADER_STRICT_TRANSPORT_SECURITY]: StrictTransportSecurity.encode(meta.hsts)
33
36
  [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
34
- [HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS]: exposed.join(','),
37
+ [HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS]: exposed.join(COMMON_LIST_VALUE_JOINER_COMMA),
35
38
  // Access-Control-Allow-Credentials // for non-preflight
36
39
  [HTTP2_HEADER_STATUS]: status,
37
40
  [HTTP2_HEADER_CONTENT_TYPE]: contentType,
@@ -55,6 +58,7 @@ export function performanceHeaders(meta) {
55
58
  * @returns {OutgoingHttpHeaders}
56
59
  */
57
60
  export function customHeaders(meta) {
58
- const m = new Map(meta.customHeaders?.filter(h => h[0].startsWith('X-')))
61
+ const CUSTOM_HEADER_PREFIX = 'X-'
62
+ const m = new Map(meta.customHeaders?.filter(h => h[0].startsWith(CUSTOM_HEADER_PREFIX)))
59
63
  return Object.fromEntries(m)
60
64
  }
@@ -1,7 +1,7 @@
1
1
  import { sendAccepted } from './2xx/accepted.js'
2
2
  import { sendBytes } from './2xx/bytes.js'
3
3
  import { sendCreated } from './2xx/created.js'
4
- import { sendJSON_Encoded } from './2xx/json.js'
4
+ import { sendJSON } from './2xx/json.js'
5
5
  import { sendNoContent } from './2xx/no-content.js'
6
6
  import { sendPartialContent } from './2xx/partial-content.js'
7
7
  import { sendPreflight } from './2xx/preflight.js'
@@ -49,7 +49,7 @@ export const Response = {
49
49
  gone: sendGone,
50
50
  imATeapot: sendImATeapot,
51
51
  insufficientStorage: sendInsufficientStorage,
52
- json: sendJSON_Encoded,
52
+ json: sendJSON,
53
53
  movedPermanently: sendMovedPermanently,
54
54
  multipleChoices: sendMultipleChoices,
55
55
  noContent: sendNoContent,
@@ -1,18 +1,13 @@
1
1
  import http2 from 'node:http2'
2
- import { pipeline, Readable } from 'node:stream'
2
+ import { compose, pipeline, Readable } from 'node:stream'
3
3
  import { ReadableStream } from 'node:stream/web'
4
- import {
5
- brotliCompressSync,
6
- deflateSync,
7
- gzipSync,
8
- zstdCompressSync
9
- } from 'node:zlib'
4
+ import zlib from 'node:zlib'
10
5
 
11
- import { HTTP_HEADER_ACCEPT_QUERY } from '../defs.js'
6
+ import { COMMON_LIST_VALUE_JOINER_COMMA, HTTP_HEADER_ACCEPT_QUERY } from '../defs.js'
12
7
  import { CacheControl } from '../headers/cache-control.js'
13
8
  import { Conditional } from '../headers/conditional.js'
14
9
  import { ContentRange } from '../headers/content-range.js'
15
- import { CHARSET_UTF8 } from '../headers/content-type.js'
10
+ import { CHARSET_UTF8, CONTENT_TYPE_JSON } from '../headers/content-type.js'
16
11
  import {
17
12
  coreHeaders,
18
13
  customHeaders,
@@ -21,7 +16,7 @@ import {
21
16
 
22
17
  /** @import { ServerHttp2Stream } from 'node:http2' */
23
18
  /** @import { OutgoingHttpHeaders } from 'node:http2' */
24
- /** @import { InputType } from 'node:zlib' */
19
+ /** @import { InputType, BrotliOptions, ZlibOptions, ZstdOptions } from 'node:zlib' */
25
20
  /** @import { AcceptRangeUnits, Metadata, SendBody } from '../defs.js' */
26
21
  /** @import { EtagItem, IMFFixDateInput } from '../headers/conditional.js' */
27
22
  /** @import { CacheControlOptions } from '../headers/cache-control.js' */
@@ -45,19 +40,95 @@ const {
45
40
  HTTP2_HEADER_CONTENT_LENGTH,
46
41
  HTTP2_HEADER_ACCEPT,
47
42
  HTTP2_HEADER_ACCEPT_ENCODING,
48
- HTTP2_HEADER_RANGE
43
+ HTTP2_HEADER_RANGE,
44
+ HTTP2_HEADER_RETRY_AFTER
49
45
  } = http2.constants
50
46
 
47
+ /**
48
+ * @param {ServerHttp2Stream} stream
49
+ * @param {number} status
50
+ * @param {OutgoingHttpHeaders} headers
51
+ * @param {Array<string>} exposedHeaders
52
+ * @param {Metadata} meta
53
+ */
54
+ export function send_no_body(stream, status, headers, exposedHeaders, meta) {
55
+ send(stream, status, headers, exposedHeaders, undefined, undefined, meta)
56
+ }
57
+
58
+
59
+ /**
60
+ * @param {ServerHttp2Stream} stream
61
+ * @param {number} status
62
+ * @param {string|undefined} message
63
+ * @param {number|undefined} retryAfter
64
+ * @param {Metadata} meta
65
+ */
66
+ export function send_error(stream, status, message, retryAfter, meta) {
67
+ const obj = JSON.stringify({
68
+ message: message ?? 'Error'
69
+ })
70
+
71
+ const exposedHeaders = Number.isInteger(retryAfter) ? [ HTTP2_HEADER_RETRY_AFTER ] : []
72
+
73
+ send(stream, status, {
74
+ [HTTP2_HEADER_RETRY_AFTER]: Number.isInteger(retryAfter) ? `${retryAfter}` : undefined
75
+ }, exposedHeaders, CONTENT_TYPE_JSON, obj, meta) // todo should this be plain text
76
+ }
77
+
51
78
  /** @typedef { (data: InputType) => Buffer<ArrayBuffer> } EncoderFun */
52
79
 
53
80
  /** @type {Map<string, EncoderFun>} */
54
81
  export const ENCODER_MAP = new Map([
55
- [ 'br', data => brotliCompressSync(data) ],
56
- [ 'gzip', data => gzipSync(data) ],
57
- [ 'deflate', data => deflateSync(data) ],
58
- [ 'zstd', data => zstdCompressSync(data) ]
82
+ [ 'br', data => zlib.brotliCompressSync(data) ],
83
+ [ 'gzip', data => zlib.gzipSync(data) ],
84
+ [ 'deflate', data => zlib.deflateSync(data) ],
85
+ [ 'zstd', data => zlib.zstdCompressSync(data) ]
59
86
  ])
60
87
 
88
+ /** @type {BrotliOptions} */
89
+ export const ENCODER_STREAM_BR_OPTIONS = { }
90
+
91
+ /** @type {ZlibOptions} */
92
+ export const ENCODER_STREAM_GZIP_OPTIONS = {}
93
+
94
+ /** @type {ZlibOptions} */
95
+ export const ENCODER_STREAM_DEFLATE_OPTIONS = {}
96
+
97
+ /** @type {ZstdOptions} */
98
+ export const ENCODER_STREAM_ZSTD_OPTIONS = {}
99
+
100
+ /** @typedef {(stream: Readable) => Readable} EncoderStreamFn */
101
+
102
+ /** @type {Map<string, EncoderStreamFn>} */
103
+ export const ENCODER_STREAM_MAP = new Map([
104
+ [ 'br', stream => compose(stream, zlib.createBrotliCompress(ENCODER_STREAM_BR_OPTIONS)) ],
105
+ [ 'gzip', stream => compose(stream, zlib.createGzip(ENCODER_STREAM_GZIP_OPTIONS)) ],
106
+ [ 'deflate', stream => compose(stream, zlib.createDeflate(ENCODER_STREAM_DEFLATE_OPTIONS)) ],
107
+ [ 'zstd', stream => compose(stream, zlib.createZstdCompress(ENCODER_STREAM_ZSTD_OPTIONS)) ]
108
+ ])
109
+
110
+
111
+ /**
112
+ * @template T
113
+ * @param {string|undefined|'identity'} encoding
114
+ * @param {Map<string, T>} listing
115
+ * @returns {{ encoderFn: T | undefined, encoding: string | undefined }}
116
+ */
117
+ export function lookupEncoder(encoding, listing) {
118
+ const encoderFn = listing.get(encoding ?? 'identity')
119
+ if(encoderFn === undefined) {
120
+ return {
121
+ encoderFn: undefined,
122
+ encoding: encoding === 'identity' ? encoding : undefined
123
+ }
124
+ }
125
+
126
+ return {
127
+ encoderFn,
128
+ encoding
129
+ }
130
+ }
131
+
61
132
  /**
62
133
  * @param {ServerHttp2Stream} stream
63
134
  * @param {number} status
@@ -75,13 +146,16 @@ export const ENCODER_MAP = new Map([
75
146
  export function send_encoded(stream, status, contentType, body, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
76
147
  const obj = (typeof body === 'string') ? Buffer.from(body, CHARSET_UTF8) : body
77
148
 
78
- const useIdentity = encoding === 'identity'
79
- const encoder = encoding === undefined ? undefined : ENCODER_MAP.get(encoding)
80
- const hasEncoder = encoder !== undefined
81
- const actualEncoding = hasEncoder ? encoding : undefined
149
+ if((obj instanceof ReadableStream) || (obj instanceof Readable)) {
150
+ const { encoderFn, encoding: actualEncoding } = lookupEncoder(encoding, ENCODER_STREAM_MAP)
151
+ const encodedStream = (encoderFn === undefined) ? obj : encoderFn((obj instanceof ReadableStream) ? Readable.fromWeb(obj) : obj)
152
+ send_bytes(stream, status, contentType, encodedStream, undefined, undefined, actualEncoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
153
+ return
154
+ }
82
155
 
156
+ const { encoderFn, encoding: actualEncoding } = lookupEncoder(encoding, ENCODER_MAP)
83
157
  const encodeStart = performance.now()
84
- const encodedData = (hasEncoder && !useIdentity) ? encoder(obj) : obj
158
+ const encodedData = (encoderFn === undefined) ? obj : encoderFn(obj)
85
159
  const encodeEnd = performance.now()
86
160
 
87
161
  meta.performance.push(
@@ -91,7 +165,6 @@ export function send_encoded(stream, status, contentType, body, encoding, etag,
91
165
  send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
92
166
  }
93
167
 
94
-
95
168
  /**
96
169
  * @param {ServerHttp2Stream} stream
97
170
  * @param {number} status
@@ -123,15 +196,15 @@ export function send_bytes(stream, status, contentType, obj, range, contentLengt
123
196
 
124
197
  send(stream, status, {
125
198
  [HTTP2_HEADER_CONTENT_ENCODING]: encoding,
126
- [HTTP2_HEADER_VARY]: varyHeaders.join(','),
199
+ [HTTP2_HEADER_VARY]: varyHeaders.join(COMMON_LIST_VALUE_JOINER_COMMA),
127
200
  [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
128
201
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
129
202
  [HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified),
130
- [HTTP2_HEADER_AGE]: age === undefined ? undefined : `${age}`,
203
+ [HTTP2_HEADER_AGE]: Number.isInteger(age) ? `${age}` : undefined,
131
204
  [HTTP2_HEADER_CONTENT_LENGTH]: contentLen,
132
205
  [HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(range),
133
206
  [HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
134
- [HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
207
+ [HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(COMMON_LIST_VALUE_JOINER_COMMA)
135
208
  }, exposedHeaders, contentType, obj, meta)
136
209
  }
137
210
 
@@ -167,14 +240,13 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
167
240
  }
168
241
 
169
242
  if(stream.writable && body !== undefined) {
170
- if(body instanceof ReadableStream) {
171
- const signal = undefined // AbortSignal.timeout(1000)
243
+ if(body instanceof ReadableStream || body instanceof Readable) {
172
244
  pipeline(
173
- Readable.fromWeb(body, { signal }),
245
+ body,
174
246
  stream,
175
247
  err => {
176
- if(err !== null) {
177
- console.warn('pipeline error')
248
+ if(err !== null && err !== undefined) {
249
+ console.warn('pipeline error', err)
178
250
  }
179
251
  })
180
252
 
@@ -187,4 +259,4 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
187
259
 
188
260
  stream.end()
189
261
  // if(!stream.closed) { stream.close() }
190
- }
262
+ }