@johntalton/http-util 4.1.1 → 5.0.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.
Files changed (57) hide show
  1. package/README.md +232 -21
  2. package/package.json +1 -1
  3. package/src/accept-encoding.js +1 -1
  4. package/src/accept-language.js +1 -1
  5. package/src/accept-util.js +1 -1
  6. package/src/accept.js +2 -5
  7. package/src/body.js +13 -15
  8. package/src/cache-control.js +6 -1
  9. package/src/clear-site-data.js +59 -0
  10. package/src/conditional.js +37 -42
  11. package/src/content-disposition.js +12 -7
  12. package/src/content-range.js +59 -0
  13. package/src/content-type.js +17 -14
  14. package/src/forwarded.js +1 -1
  15. package/src/index.js +6 -1
  16. package/src/multipart.js +81 -33
  17. package/src/preference.js +205 -0
  18. package/src/range.js +154 -0
  19. package/src/response/accepted.js +1 -0
  20. package/src/response/bytes.js +27 -0
  21. package/src/response/conflict.js +1 -0
  22. package/src/response/content-too-large.js +16 -0
  23. package/src/response/created.js +2 -1
  24. package/src/response/defs.js +15 -1
  25. package/src/response/error.js +1 -0
  26. package/src/response/forbidden.js +17 -0
  27. package/src/response/gone.js +16 -0
  28. package/src/response/header-util.js +2 -1
  29. package/src/response/im-a-teapot.js +16 -0
  30. package/src/response/index.js +17 -0
  31. package/src/response/insufficient-storage.js +16 -0
  32. package/src/response/json.js +6 -53
  33. package/src/response/moved-permanently.js +23 -0
  34. package/src/response/multiple-choices.js +21 -0
  35. package/src/response/no-content.js +2 -1
  36. package/src/response/not-acceptable.js +2 -1
  37. package/src/response/not-allowed.js +1 -0
  38. package/src/response/not-found.js +1 -0
  39. package/src/response/not-implemented.js +1 -0
  40. package/src/response/not-modified.js +3 -2
  41. package/src/response/partial-content.js +71 -0
  42. package/src/response/permanent-redirect.js +23 -0
  43. package/src/response/precondition-failed.js +1 -0
  44. package/src/response/preflight.js +21 -5
  45. package/src/response/range-not-satisfiable.js +28 -0
  46. package/src/response/response.js +26 -0
  47. package/src/response/see-other.js +23 -0
  48. package/src/response/send-util.js +137 -6
  49. package/src/response/sse.js +4 -3
  50. package/src/response/temporary-redirect.js +23 -0
  51. package/src/response/timeout.js +1 -0
  52. package/src/response/too-many-requests.js +1 -0
  53. package/src/response/trace.js +8 -4
  54. package/src/response/unauthorized.js +1 -0
  55. package/src/response/unavailable.js +1 -0
  56. package/src/response/unprocessable.js +1 -0
  57. package/src/response/unsupported-media.js +15 -4
@@ -0,0 +1,23 @@
1
+ import http2 from 'node:http2'
2
+
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_LOCATION
10
+ } = http2.constants
11
+
12
+ const { HTTP_STATUS_SEE_OTHER } = http2.constants
13
+
14
+ /**
15
+ * @param {ServerHttp2Stream} stream
16
+ * @param {URL} location
17
+ * @param {Metadata} meta
18
+ */
19
+ export function sendSeeOther(stream, location, meta) {
20
+ send(stream, HTTP_STATUS_SEE_OTHER, {
21
+ [HTTP2_HEADER_LOCATION]: location.href
22
+ }, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
23
+ }
@@ -1,3 +1,18 @@
1
+ import http2 from 'node:http2'
2
+ import { Readable } from 'node:stream'
3
+ import { ReadableStream } from 'node:stream/web'
4
+ import {
5
+ brotliCompressSync,
6
+ deflateSync,
7
+ gzipSync,
8
+ zstdCompressSync
9
+ } from 'node:zlib'
10
+
11
+ import { CacheControl } from '../cache-control.js'
12
+ import { Conditional } from '../conditional.js'
13
+ import { ContentRange } from '../content-range.js'
14
+ import { CHARSET_UTF8 } from '../content-type.js'
15
+ import { HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
1
16
  import {
2
17
  coreHeaders,
3
18
  customHeaders,
@@ -6,7 +21,117 @@ import {
6
21
 
7
22
  /** @import { ServerHttp2Stream } from 'node:http2' */
8
23
  /** @import { IncomingHttpHeaders } from 'node:http2' */
24
+ /** @import { InputType } from 'node:zlib' */
9
25
  /** @import { Metadata } from './defs.js' */
26
+ /** @import { EtagItem } from '../conditional.js' */
27
+ /** @import { CacheControlOptions } from '../cache-control.js' */
28
+ /** @import { ContentRangeDirective } from '../content-range.js' */
29
+
30
+ /** @typedef {ArrayBufferLike|ArrayBufferView|ReadableStream|string} SendBody */
31
+
32
+ const {
33
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
34
+ HTTP_STATUS_NOT_FOUND,
35
+ HTTP_STATUS_UNAUTHORIZED
36
+ } = http2.constants
37
+
38
+ const {
39
+ HTTP2_HEADER_CONTENT_ENCODING,
40
+ HTTP2_HEADER_VARY,
41
+ HTTP2_HEADER_CACHE_CONTROL,
42
+ HTTP2_HEADER_ETAG,
43
+ HTTP2_HEADER_AGE,
44
+ HTTP2_HEADER_ACCEPT_RANGES,
45
+ HTTP2_HEADER_CONTENT_RANGE,
46
+ HTTP2_HEADER_CONTENT_LENGTH,
47
+ HTTP2_HEADER_ACCEPT,
48
+ HTTP2_HEADER_ACCEPT_ENCODING,
49
+ HTTP2_HEADER_RANGE
50
+ } = http2.constants
51
+
52
+ /** @typedef { (data: InputType) => Buffer } EncoderFun */
53
+
54
+ /** @type {Map<string, EncoderFun>} */
55
+ export const ENCODER_MAP = new Map([
56
+ [ 'br', data => brotliCompressSync(data) ],
57
+ [ 'gzip', data => gzipSync(data) ],
58
+ [ 'deflate', data => deflateSync(data) ],
59
+ [ 'zstd', data => zstdCompressSync(data) ]
60
+ ])
61
+
62
+ /**
63
+ * @param {ServerHttp2Stream} stream
64
+ * @param {number} status
65
+ * @param {string|undefined} contentType
66
+ * @param {SendBody} body
67
+ * @param {string|undefined} encoding
68
+ * @param {EtagItem|undefined} etag
69
+ * @param {number|undefined} age
70
+ * @param {CacheControlOptions|undefined} cacheControl
71
+ * @param {'bytes'|'none'|undefined} acceptRanges
72
+ * @param {Array<string>|undefined} supportedQueryTypes
73
+ * @param {Metadata} meta
74
+ */
75
+ export function send_encoded(stream, status, contentType, body, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
76
+ const obj = (typeof body === 'string') ? Buffer.from(body, CHARSET_UTF8) : body
77
+
78
+ const useIdentity = encoding === 'identity'
79
+ const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
80
+ const hasEncoder = encoder !== undefined
81
+ const actualEncoding = hasEncoder ? encoding : undefined
82
+
83
+ const encodeStart = performance.now()
84
+ const encodedData = (hasEncoder && !useIdentity) ? encoder(obj) : obj
85
+ const encodeEnd = performance.now()
86
+
87
+ meta.performance.push(
88
+ { name: 'encode', duration: encodeEnd - encodeStart }
89
+ )
90
+
91
+ send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
92
+ }
93
+
94
+
95
+ /**
96
+ * @param {ServerHttp2Stream} stream
97
+ * @param {number} status
98
+ * @param {string|undefined} contentType
99
+ * @param {SendBody|undefined} obj
100
+ * @param {ContentRangeDirective|undefined} range
101
+ * @param {number|undefined} contentLength
102
+ * @param {string|undefined} encoding
103
+ * @param {EtagItem|undefined} etag
104
+ * @param {number|undefined} age
105
+ * @param {CacheControlOptions|undefined} cacheControl
106
+ * @param {'bytes'|'none'|undefined} acceptRanges
107
+ * @param {Array<string>|undefined} supportedQueryTypes
108
+ * @param {Metadata} meta
109
+ */
110
+ export function send_bytes(stream, status, contentType, obj, range, contentLength, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
111
+ const contentLen = Number.isInteger(contentLength) ? `${contentLength}` : undefined
112
+ const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
113
+
114
+ const exposedHeaders = [ ]
115
+ if(age !== undefined) { exposedHeaders.push(HTTP2_HEADER_AGE) }
116
+ if(acceptRanges !== undefined) { exposedHeaders.push(HTTP2_HEADER_ACCEPT_RANGES) }
117
+ if(range !== undefined) { exposedHeaders.push(HTTP2_HEADER_CONTENT_RANGE) }
118
+ if(supportsQuery) { exposedHeaders.push(HTTP_HEADER_ACCEPT_QUERY) }
119
+
120
+ const varyHeaders = [ HTTP2_HEADER_ACCEPT, HTTP2_HEADER_ACCEPT_ENCODING ]
121
+ if(range !== undefined) { varyHeaders.push(HTTP2_HEADER_RANGE) } // todo: very on range is true even if not returning a content range (multipart/byteranges)
122
+
123
+ send(stream, status, {
124
+ [HTTP2_HEADER_CONTENT_ENCODING]: encoding,
125
+ [HTTP2_HEADER_VARY]: varyHeaders.join(','),
126
+ [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
127
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
128
+ [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined,
129
+ [HTTP2_HEADER_CONTENT_LENGTH]: contentLen,
130
+ [HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(range),
131
+ [HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
132
+ [HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
133
+ }, exposedHeaders, contentType, obj, meta)
134
+ }
10
135
 
11
136
  /**
12
137
  * @param {ServerHttp2Stream} stream
@@ -14,17 +139,18 @@ import {
14
139
  * @param {IncomingHttpHeaders} headers
15
140
  * @param {Array<string>} exposedHeaders
16
141
  * @param {string|undefined} contentType
17
- * @param {ArrayBufferLike|ArrayBufferView|string|undefined} body
142
+ * @param {SendBody|undefined} body
18
143
  * @param {Metadata} meta
19
144
  */
20
145
  export function send(stream, status, headers, exposedHeaders, contentType, body, meta) {
21
146
  // if(status >= 400) { console.warn(status, body) }
22
- if(status === 401) { console.warn(status, body) }
23
- if(status === 404) { console.warn(status, body) }
24
- if(status >= 500) { console.warn(status, body) }
147
+ if(status === HTTP_STATUS_UNAUTHORIZED) { console.warn(status, body) }
148
+ if(status === HTTP_STATUS_NOT_FOUND) { console.warn(status, body) }
149
+ if(status >= HTTP_STATUS_INTERNAL_SERVER_ERROR) { console.warn(status, body) }
150
+ // console.log('SEND', status, body?.byteLength)
25
151
 
26
- if(stream === undefined) { return }
27
- if(stream.closed) { return }
152
+ if(stream === undefined) { console.log('send - end stream undef'); return }
153
+ if(stream.closed) { console.log('send - end closed'); return }
28
154
 
29
155
  if(!stream.headersSent) {
30
156
  const custom = customHeaders(meta)
@@ -39,6 +165,11 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
39
165
  }
40
166
 
41
167
  if(stream.writable && body !== undefined) {
168
+ if(body instanceof ReadableStream) {
169
+ Readable.fromWeb(body).pipe(stream)
170
+ return
171
+ }
172
+
42
173
  stream.end(body)
43
174
  return
44
175
  }
@@ -1,9 +1,10 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import {
3
- SSE_MIME,
4
- SSE_INACTIVE_STATUS_CODE,
5
- SSE_BOM,
6
4
  ENDING,
5
+ SSE_BOM,
6
+ SSE_INACTIVE_STATUS_CODE,
7
+ SSE_MIME,
7
8
  } from '@johntalton/sse-util'
8
9
  import { coreHeaders, performanceHeaders } from './header-util.js'
9
10
 
@@ -0,0 +1,23 @@
1
+ import http2 from 'node:http2'
2
+
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_LOCATION
10
+ } = http2.constants
11
+
12
+ const { HTTP_STATUS_TEMPORARY_REDIRECT } = http2.constants
13
+
14
+ /**
15
+ * @param {ServerHttp2Stream} stream
16
+ * @param {URL} location
17
+ * @param {Metadata} meta
18
+ */
19
+ export function sendTemporaryRedirect(stream, location, meta) {
20
+ send(stream, HTTP_STATUS_TEMPORARY_REDIRECT, {
21
+ [HTTP2_HEADER_LOCATION]: location.href
22
+ }, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
23
+ }
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
4
  import {
4
5
  HTTP_HEADER_RATE_LIMIT,
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_MESSAGE_HTTP } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -8,6 +9,9 @@ import { send } from './send-util.js'
8
9
 
9
10
  const { HTTP_STATUS_OK } = http2.constants
10
11
 
12
+ const LINE_ENDING = '\n'
13
+ const PSEUDO_HEADER_PREFIX = ':'
14
+
11
15
  /**
12
16
  * @param {ServerHttp2Stream} stream
13
17
  * @param {string} method
@@ -27,13 +31,13 @@ export function sendTrace(stream, method, url, headers, meta) {
27
31
  const reconstructed = [
28
32
  `${method} ${url.pathname}${url.search} ${version}`,
29
33
  Object.entries(headers)
30
- .filter(([ key ]) => !key.startsWith(':'))
34
+ .filter(([ key ]) => !key.startsWith(PSEUDO_HEADER_PREFIX))
31
35
  .filter(([ key ]) => !FILTER_KEYS.includes(key))
32
36
  .map(([ key, value ]) => `${key}: ${value}`)
33
- .join('\n'),
34
- '\n'
37
+ .join(LINE_ENDING),
38
+ LINE_ENDING
35
39
  ]
36
- .join('\n')
40
+ .join(LINE_ENDING)
37
41
 
38
42
  send(stream, HTTP_STATUS_OK, {}, [], CONTENT_TYPE_MESSAGE_HTTP, reconstructed, meta)
39
43
  }
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,21 +1,32 @@
1
1
  import http2 from 'node:http2'
2
- import { HTTP_HEADER_ACCEPT_POST } from './defs.js'
2
+
3
+ import { HTTP_HEADER_ACCEPT_PATCH, HTTP_HEADER_ACCEPT_POST, HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
3
4
  import { send } from './send-util.js'
4
5
 
5
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
7
  /** @import { Metadata } from './defs.js' */
7
8
 
9
+ const { HTTP2_METHOD_POST, HTTP2_METHOD_PATCH } = http2.constants
10
+
8
11
  const { HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE } = http2.constants
9
12
 
10
13
  /**
11
14
  * @param {ServerHttp2Stream} stream
12
15
  * @param {Array<string>|string} acceptableMediaType
16
+ * @param {Array<string>|undefined} supportedQueryTypes
13
17
  * @param {Metadata} meta
14
18
  */
15
- export function sendUnsupportedMediaType(stream, acceptableMediaType, meta) {
19
+ export function sendUnsupportedMediaType(stream, acceptableMediaType, supportedQueryTypes, meta) {
20
+ const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
21
+ const exposedHeaders = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY, HTTP_HEADER_ACCEPT_POST ] : [ HTTP_HEADER_ACCEPT_POST ]
22
+
23
+ const method = HTTP2_METHOD_POST // todo pass in as parameter or split acceptable to post and patch types
16
24
  const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ]
25
+ const acceptHeader = (method === HTTP2_METHOD_POST) ? HTTP_HEADER_ACCEPT_POST : HTTP_HEADER_ACCEPT_PATCH
26
+ const acceptValue = ((method === HTTP2_METHOD_POST) || (method === HTTP2_METHOD_PATCH)) ? acceptable.join(',') : undefined
17
27
 
18
28
  send(stream, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, {
19
- [HTTP_HEADER_ACCEPT_POST]: acceptable.join(',')
20
- }, [ HTTP_HEADER_ACCEPT_POST ], undefined, undefined, meta)
29
+ [acceptHeader]: acceptValue,
30
+ [HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
31
+ }, exposedHeaders, undefined, undefined, meta)
21
32
  }