@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.
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/body.js +11 -6
- package/src/defs.js +14 -5
- package/src/headers/accept-encoding.js +28 -5
- package/src/headers/accept-language.js +29 -5
- package/src/headers/accept.js +77 -32
- package/src/headers/cache-control.js +6 -3
- package/src/headers/client-hints.js +4 -3
- package/src/headers/conditional.js +18 -18
- package/src/headers/content-type.js +1 -1
- package/src/headers/link.js +9 -4
- package/src/headers/multipart.js +18 -17
- package/src/headers/range.js +4 -2
- package/src/headers/rate-limit.js +20 -4
- package/src/headers/server-timing.js +5 -3
- package/src/headers/util/kvp.js +2 -1
- package/src/headers/util/mime.js +17 -1
- package/src/headers/util/quote.js +1 -1
- package/src/headers/util/whitespace.js +1 -1
- package/src/headers/www-authenticate.js +35 -11
- package/src/response/2xx/accepted.js +2 -2
- package/src/response/2xx/bytes.js +2 -31
- package/src/response/2xx/created.js +8 -21
- package/src/response/2xx/json.js +6 -19
- package/src/response/2xx/no-content.js +4 -18
- package/src/response/2xx/partial-content.js +1 -28
- package/src/response/2xx/preflight.js +18 -25
- package/src/response/3xx/found.js +8 -6
- package/src/response/3xx/moved-permanently.js +8 -6
- package/src/response/3xx/multiple-choices.js +3 -3
- package/src/response/3xx/not-modified.js +14 -26
- package/src/response/3xx/permanent-redirect.js +7 -5
- package/src/response/3xx/see-other.js +8 -6
- package/src/response/3xx/temporary-redirect.js +7 -5
- package/src/response/4xx/bad-request.js +2 -3
- package/src/response/4xx/conflict.js +2 -2
- package/src/response/4xx/content-too-large.js +2 -2
- package/src/response/4xx/forbidden.js +3 -3
- package/src/response/4xx/gone.js +2 -2
- package/src/response/4xx/im-a-teapot.js +2 -2
- package/src/response/4xx/not-acceptable.js +2 -11
- package/src/response/4xx/not-allowed.js +6 -14
- package/src/response/4xx/payment-required.js +4 -4
- package/src/response/4xx/precondition-failed.js +4 -18
- package/src/response/4xx/range-not-satisfiable.js +4 -13
- package/src/response/4xx/timeout.js +3 -3
- package/src/response/4xx/too-many-requests.js +3 -20
- package/src/response/4xx/unauthorized.js +4 -4
- package/src/response/4xx/unprocessable.js +2 -2
- package/src/response/4xx/unsupported-media.js +16 -23
- package/src/response/5xx/error.js +2 -3
- package/src/response/5xx/insufficient-storage.js +2 -2
- package/src/response/5xx/not-implemented.js +2 -3
- package/src/response/5xx/unavailable.js +3 -20
- package/src/response/header-util.js +9 -5
- package/src/response/response.js +2 -2
- package/src/response/send-util.js +102 -30
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
16
|
+
export function sendUnavailable(stream, message, info, meta) {
|
|
32
17
|
const { retryAfter } = info
|
|
33
18
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/response/response.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|