@johntalton/http-util 7.0.1 → 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/package.json +1 -1
- package/src/body.js +11 -6
- package/src/defs.js +13 -4
- 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 +17 -17
- package/src/headers/content-type.js +1 -1
- package/src/headers/link.js +8 -3
- package/src/headers/multipart.js +14 -6
- package/src/headers/range.js +2 -2
- package/src/headers/rate-limit.js +19 -3
- package/src/headers/server-timing.js +5 -3
- package/src/headers/util/kvp.js +2 -1
- package/src/headers/util/mime.js +16 -0
- package/src/headers/util/quote.js +1 -1
- package/src/headers/www-authenticate.js +35 -10
- package/src/response/2xx/accepted.js +2 -2
- package/src/response/2xx/created.js +3 -3
- package/src/response/2xx/no-content.js +3 -3
- package/src/response/2xx/preflight.js +9 -9
- package/src/response/3xx/found.js +3 -3
- package/src/response/3xx/moved-permanently.js +3 -3
- package/src/response/3xx/multiple-choices.js +3 -3
- package/src/response/3xx/not-modified.js +12 -5
- package/src/response/3xx/permanent-redirect.js +3 -3
- package/src/response/3xx/see-other.js +3 -3
- package/src/response/3xx/temporary-redirect.js +3 -3
- package/src/response/4xx/bad-request.js +1 -2
- 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-allowed.js +5 -4
- package/src/response/4xx/payment-required.js +3 -3
- package/src/response/4xx/precondition-failed.js +3 -3
- package/src/response/4xx/range-not-satisfiable.js +3 -3
- package/src/response/4xx/timeout.js +3 -3
- package/src/response/4xx/too-many-requests.js +2 -5
- package/src/response/4xx/unauthorized.js +4 -4
- package/src/response/4xx/unprocessable.js +2 -2
- package/src/response/4xx/unsupported-media.js +15 -9
- package/src/response/header-util.js +8 -4
- package/src/response/send-util.js +77 -24
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
3
|
import { Challenge } from '../../headers/www-authenticate.js'
|
|
4
|
-
import {
|
|
4
|
+
import { send_no_body } from '../send-util.js'
|
|
5
5
|
|
|
6
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
7
|
/** @import { Metadata } from '../../defs.js' */
|
|
@@ -19,7 +19,7 @@ const { HTTP_STATUS_UNAUTHORIZED } = http2.constants
|
|
|
19
19
|
* @param {Metadata} meta
|
|
20
20
|
*/
|
|
21
21
|
export function sendUnauthorized(stream, challenge, meta) {
|
|
22
|
-
|
|
23
|
-
[HTTP2_HEADER_WWW_AUTHENTICATE]:
|
|
24
|
-
}, [ HTTP2_HEADER_WWW_AUTHENTICATE ],
|
|
22
|
+
send_no_body(stream, HTTP_STATUS_UNAUTHORIZED, {
|
|
23
|
+
[HTTP2_HEADER_WWW_AUTHENTICATE]: Challenge.encode(challenge),
|
|
24
|
+
}, [ HTTP2_HEADER_WWW_AUTHENTICATE ], meta)
|
|
25
25
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { send_no_body } from '../send-util.js'
|
|
4
4
|
|
|
5
5
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
6
|
/** @import { Metadata } from '../../defs.js' */
|
|
@@ -12,5 +12,5 @@ const { HTTP_STATUS_UNPROCESSABLE_ENTITY } = http2.constants
|
|
|
12
12
|
* @param {Metadata} meta
|
|
13
13
|
*/
|
|
14
14
|
export function sendUnprocessable(stream, meta) {
|
|
15
|
-
|
|
15
|
+
send_no_body(stream, HTTP_STATUS_UNPROCESSABLE_ENTITY, {}, [], meta)
|
|
16
16
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
COMMON_LIST_VALUE_JOINER_COMMA,
|
|
5
|
+
HTTP_HEADER_ACCEPT_PATCH,
|
|
6
|
+
HTTP_HEADER_ACCEPT_POST,
|
|
7
|
+
HTTP_HEADER_ACCEPT_QUERY
|
|
8
|
+
} from '../../defs.js'
|
|
9
|
+
import { send_no_body } from '../send-util.js'
|
|
5
10
|
|
|
6
11
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
12
|
/** @import { SendInfo, Metadata } from '../../defs.js' */
|
|
@@ -21,16 +26,17 @@ export function sendUnsupportedMediaType(stream, info, meta) {
|
|
|
21
26
|
acceptableMediaType
|
|
22
27
|
} = info
|
|
23
28
|
|
|
24
|
-
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
25
|
-
const exposedHeaders = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY, HTTP_HEADER_ACCEPT_POST ] : [ HTTP_HEADER_ACCEPT_POST ]
|
|
26
|
-
|
|
27
29
|
const method = HTTP2_METHOD_POST // todo pass in as parameter or split acceptable to post and patch types
|
|
28
30
|
const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ] // todo undefined creates array of one item
|
|
29
31
|
const acceptHeader = (method === HTTP2_METHOD_POST) ? HTTP_HEADER_ACCEPT_POST : HTTP_HEADER_ACCEPT_PATCH
|
|
30
|
-
const acceptValue = ((method === HTTP2_METHOD_POST) || (method === HTTP2_METHOD_PATCH)) ? acceptable.join(
|
|
32
|
+
const acceptValue = ((method === HTTP2_METHOD_POST) || (method === HTTP2_METHOD_PATCH)) ? acceptable.join(COMMON_LIST_VALUE_JOINER_COMMA) : undefined
|
|
33
|
+
|
|
34
|
+
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
35
|
+
const exposedHeaders = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY, acceptHeader ] : [ acceptHeader ]
|
|
36
|
+
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
send_no_body(stream, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, {
|
|
33
39
|
[acceptHeader]: acceptValue,
|
|
34
|
-
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(
|
|
35
|
-
}, exposedHeaders,
|
|
40
|
+
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(COMMON_LIST_VALUE_JOINER_COMMA)
|
|
41
|
+
}, exposedHeaders, meta)
|
|
36
42
|
}
|
|
@@ -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
|
/**
|
|
@@ -30,8 +32,9 @@ export function coreHeaders(status, contentType, exposedHeaders, meta) {
|
|
|
30
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
|
}
|
|
@@ -1,14 +1,9 @@
|
|
|
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'
|
|
@@ -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' */
|
|
@@ -49,6 +44,18 @@ const {
|
|
|
49
44
|
HTTP2_HEADER_RETRY_AFTER
|
|
50
45
|
} = http2.constants
|
|
51
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
|
+
|
|
52
59
|
/**
|
|
53
60
|
* @param {ServerHttp2Stream} stream
|
|
54
61
|
* @param {number} status
|
|
@@ -72,12 +79,56 @@ export function send_error(stream, status, message, retryAfter, meta) {
|
|
|
72
79
|
|
|
73
80
|
/** @type {Map<string, EncoderFun>} */
|
|
74
81
|
export const ENCODER_MAP = new Map([
|
|
75
|
-
[ 'br', data => brotliCompressSync(data) ],
|
|
76
|
-
[ 'gzip', data => gzipSync(data) ],
|
|
77
|
-
[ 'deflate', data => deflateSync(data) ],
|
|
78
|
-
[ '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) ]
|
|
86
|
+
])
|
|
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)) ]
|
|
79
108
|
])
|
|
80
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
|
+
|
|
81
132
|
/**
|
|
82
133
|
* @param {ServerHttp2Stream} stream
|
|
83
134
|
* @param {number} status
|
|
@@ -95,13 +146,16 @@ export const ENCODER_MAP = new Map([
|
|
|
95
146
|
export function send_encoded(stream, status, contentType, body, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
96
147
|
const obj = (typeof body === 'string') ? Buffer.from(body, CHARSET_UTF8) : body
|
|
97
148
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
}
|
|
102
155
|
|
|
156
|
+
const { encoderFn, encoding: actualEncoding } = lookupEncoder(encoding, ENCODER_MAP)
|
|
103
157
|
const encodeStart = performance.now()
|
|
104
|
-
const encodedData = (
|
|
158
|
+
const encodedData = (encoderFn === undefined) ? obj : encoderFn(obj)
|
|
105
159
|
const encodeEnd = performance.now()
|
|
106
160
|
|
|
107
161
|
meta.performance.push(
|
|
@@ -142,7 +196,7 @@ export function send_bytes(stream, status, contentType, obj, range, contentLengt
|
|
|
142
196
|
|
|
143
197
|
send(stream, status, {
|
|
144
198
|
[HTTP2_HEADER_CONTENT_ENCODING]: encoding,
|
|
145
|
-
[HTTP2_HEADER_VARY]: varyHeaders.join(
|
|
199
|
+
[HTTP2_HEADER_VARY]: varyHeaders.join(COMMON_LIST_VALUE_JOINER_COMMA),
|
|
146
200
|
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
147
201
|
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
148
202
|
[HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified),
|
|
@@ -150,7 +204,7 @@ export function send_bytes(stream, status, contentType, obj, range, contentLengt
|
|
|
150
204
|
[HTTP2_HEADER_CONTENT_LENGTH]: contentLen,
|
|
151
205
|
[HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(range),
|
|
152
206
|
[HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
|
|
153
|
-
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(
|
|
207
|
+
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(COMMON_LIST_VALUE_JOINER_COMMA)
|
|
154
208
|
}, exposedHeaders, contentType, obj, meta)
|
|
155
209
|
}
|
|
156
210
|
|
|
@@ -186,10 +240,9 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
|
|
|
186
240
|
}
|
|
187
241
|
|
|
188
242
|
if(stream.writable && body !== undefined) {
|
|
189
|
-
if(body instanceof ReadableStream) {
|
|
190
|
-
const signal = undefined // AbortSignal.timeout(1000)
|
|
243
|
+
if(body instanceof ReadableStream || body instanceof Readable) {
|
|
191
244
|
pipeline(
|
|
192
|
-
|
|
245
|
+
body,
|
|
193
246
|
stream,
|
|
194
247
|
err => {
|
|
195
248
|
if(err !== null && err !== undefined) {
|
|
@@ -206,4 +259,4 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
|
|
|
206
259
|
|
|
207
260
|
stream.end()
|
|
208
261
|
// if(!stream.closed) { stream.close() }
|
|
209
|
-
}
|
|
262
|
+
}
|