@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
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ All responders take in a `stream` as well as a metadata object to hint on server
|
|
|
64
64
|
- [`sendGone`](#responsegone)
|
|
65
65
|
- [`sendImATeapot`](#)
|
|
66
66
|
- [`sendInsufficientStorage`](#)
|
|
67
|
-
- [`
|
|
67
|
+
- [`sendJSON`](#responsejson) - Standard Ok response with encoding
|
|
68
68
|
- [`sendMovedPermanently`](#responsemovedpermanently)
|
|
69
69
|
- [`sendMultipleChoices`](#)
|
|
70
70
|
- [`sendNoContent`](#responsenocontent)
|
package/package.json
CHANGED
package/src/body.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
CHARSET_UTF8,
|
|
3
5
|
MIME_TYPE_MULTIPART_FORM_DATA,
|
|
@@ -8,7 +10,6 @@ import { Multipart } from './headers/multipart.js'
|
|
|
8
10
|
export const BYTE_PER_K = 1024
|
|
9
11
|
export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
|
|
10
12
|
|
|
11
|
-
/** @import { Readable } from 'node:stream' */
|
|
12
13
|
/** @import { ContentTypeItem } from './headers/content-type.js' */
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -24,7 +25,7 @@ export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
|
|
|
24
25
|
* @property {number} duration
|
|
25
26
|
* @property {ReadableStream} body
|
|
26
27
|
* @property {ContentTypeItem|undefined} contentType
|
|
27
|
-
* @property { (mimetype
|
|
28
|
+
* @property { (mimetype?: string | undefined) => Promise<Blob> } blob
|
|
28
29
|
* @property { () => Promise<ArrayBufferLike> } arrayBuffer
|
|
29
30
|
* @property { () => Promise<Uint8Array> } bytes
|
|
30
31
|
* @property { () => Promise<string> } text
|
|
@@ -38,6 +39,8 @@ export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
|
|
|
38
39
|
* @returns {BodyFuture}
|
|
39
40
|
*/
|
|
40
41
|
export function requestBody(stream, options) {
|
|
42
|
+
if(!(stream instanceof Readable)) { throw new Error('stream is not Readable') }
|
|
43
|
+
|
|
41
44
|
const signal = options?.signal
|
|
42
45
|
const byteLimit = options?.byteLimit ?? DEFAULT_BYTE_LIMIT
|
|
43
46
|
const contentLength = options?.contentLength
|
|
@@ -93,7 +96,7 @@ export function requestBody(stream, options) {
|
|
|
93
96
|
|
|
94
97
|
const listener = () => {
|
|
95
98
|
stats.closed = true
|
|
96
|
-
controller.error(new Error(
|
|
99
|
+
controller.error(new Error(`Abort Signal (${signal?.reason})`))
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
signal?.addEventListener('abort', listener, { once: true })
|
|
@@ -101,7 +104,7 @@ export function requestBody(stream, options) {
|
|
|
101
104
|
stream.on('data', chunk => {
|
|
102
105
|
if(signal?.aborted) {
|
|
103
106
|
console.log('body reader aborted')
|
|
104
|
-
controller.error(new Error(
|
|
107
|
+
controller.error(new Error(`Chunk read Abort Signal (${signal.reason})`))
|
|
105
108
|
stats.closed = true
|
|
106
109
|
return
|
|
107
110
|
}
|
|
@@ -114,8 +117,9 @@ export function requestBody(stream, options) {
|
|
|
114
117
|
|
|
115
118
|
// chunk is a node Buffer (which is a TypedArray)
|
|
116
119
|
if(!ArrayBuffer.isView(chunk)) {
|
|
117
|
-
controller.error('invalid chunk type')
|
|
120
|
+
controller.error(new Error('invalid chunk type'))
|
|
118
121
|
stats.closed = true
|
|
122
|
+
return
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
stats.byteLength += chunk.byteLength
|
|
@@ -157,6 +161,7 @@ export function requestBody(stream, options) {
|
|
|
157
161
|
|
|
158
162
|
cancel(reason) {
|
|
159
163
|
console.log('body reader canceled', reason)
|
|
164
|
+
// super.cancel(reason)
|
|
160
165
|
}
|
|
161
166
|
}
|
|
162
167
|
|
|
@@ -190,7 +195,7 @@ export function requestBody(stream, options) {
|
|
|
190
195
|
get body() { return makeReader() },
|
|
191
196
|
get contentType() { return contentType },
|
|
192
197
|
|
|
193
|
-
blob: (/** @type {string | undefined} */ mimetype) => wrap(reader => bodyBlob(reader, mimetype ?? contentType?.mimetype)),
|
|
198
|
+
blob: (/** @type {string | undefined} */ mimetype = undefined) => wrap(reader => bodyBlob(reader, mimetype ?? contentType?.mimetype)),
|
|
194
199
|
arrayBuffer: () => wrap(reader => bodyArrayBuffer(reader)),
|
|
195
200
|
bytes: () => wrap(reader => bodyUint8Array(reader)),
|
|
196
201
|
text: () => wrap(reader => bodyText(reader, charset)),
|
package/src/defs.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @import { Readable } from 'node:stream' */
|
|
2
|
+
/** @import { ReadableStream } from 'node:stream/web' */
|
|
1
3
|
|
|
2
4
|
export const HTTP_HEADER_ORIGIN = 'origin'
|
|
3
5
|
export const HTTP_HEADER_USER_AGENT = 'user-agent'
|
|
@@ -26,9 +28,14 @@ export const RANGE_UNITS_BYTES = 'bytes'
|
|
|
26
28
|
/** @type {'none'} */
|
|
27
29
|
export const RANGE_UNITS_NONE = 'none'
|
|
28
30
|
|
|
31
|
+
/** @description joiner used for concatenating multiple of the same headers into one */
|
|
32
|
+
export const COMMON_LIST_HEADER_JOINER_COMMA = ', '
|
|
33
|
+
|
|
34
|
+
/** @description joiner used for concatenating multiple values of single header */
|
|
35
|
+
export const COMMON_LIST_VALUE_JOINER_COMMA = ','
|
|
29
36
|
|
|
30
37
|
/** @import { TimingsInfo } from './headers/server-timing.js' */
|
|
31
|
-
/** @import { EtagItem, IMFFixDateInput
|
|
38
|
+
/** @import { EtagItem, IMFFixDateInput } from './headers/conditional.js' */
|
|
32
39
|
/** @import { CacheControlOptions } from './headers/cache-control.js' */
|
|
33
40
|
/** @import { ContentRangeDirective } from './headers/content-range.js' */
|
|
34
41
|
/** @import { RateLimitPolicyInfo, RateLimitInfo } from './headers/rate-limit.js' */
|
|
@@ -44,7 +51,7 @@ export const RANGE_UNITS_NONE = 'none'
|
|
|
44
51
|
* @property {Array<TimingsInfo>} performance
|
|
45
52
|
* @property {string|undefined} servername
|
|
46
53
|
* @property {string|undefined} origin
|
|
47
|
-
* @property {Array<[ CustomHeaderKey, string ]>|undefined} [customHeaders]
|
|
54
|
+
* @property {Array<[ CustomHeaderKey, string | Array<string> ]>|undefined} [customHeaders]
|
|
48
55
|
*/
|
|
49
56
|
|
|
50
57
|
/**
|
|
@@ -53,7 +60,9 @@ export const RANGE_UNITS_NONE = 'none'
|
|
|
53
60
|
* @property {boolean} [bom]
|
|
54
61
|
*/
|
|
55
62
|
|
|
56
|
-
/** @typedef {
|
|
63
|
+
/** @typedef {NodeJS.TypedArray<ArrayBuffer>} TypedArray */
|
|
64
|
+
/** @typedef {ArrayBuffer | TypedArray | string} SendBodyTypes */
|
|
65
|
+
/** @typedef { SendBodyTypes | ReadableStream<SendBodyTypes> | Readable } SendBody */
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
68
|
* @typedef {Object} SendContent
|
|
@@ -70,8 +79,8 @@ export const RANGE_UNITS_NONE = 'none'
|
|
|
70
79
|
/**
|
|
71
80
|
* @typedef {Object} SendInfo
|
|
72
81
|
* @property {Array<string>} supportedMethods
|
|
73
|
-
* @property {Array<string>|string} supportedTypes
|
|
74
|
-
* @property {Array<string>|string} acceptableMediaType
|
|
82
|
+
* @property {Array<string>|string} supportedTypes for content negotiation failures
|
|
83
|
+
* @property {Array<string>|string} acceptableMediaType for incoming post/put unsupported types
|
|
75
84
|
* @property {AcceptRangeUnits|undefined} acceptRanges
|
|
76
85
|
* @property {Array<string>|undefined} supportedQueryTypes
|
|
77
86
|
* @property {RateLimitInfo} limitInfo
|
|
@@ -18,6 +18,8 @@ export class AcceptEncoding {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
+
* @deprecated
|
|
22
|
+
* @see {@link AcceptEncoding.selectItemFrom}
|
|
21
23
|
* @param {string|undefined} acceptEncodingHeader
|
|
22
24
|
* @param {Array<string>} supportedTypes
|
|
23
25
|
*/
|
|
@@ -27,24 +29,45 @@ export class AcceptEncoding {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
* @param {Array<AcceptStyleItem>} acceptEncodings
|
|
31
|
-
* @param {Array<string>} supportedTypes
|
|
32
|
+
* @param {Array<AcceptStyleItem>} acceptEncodings (descending quality order)
|
|
33
|
+
* @param {Array<string>} supportedTypes (descending preferred order)
|
|
34
|
+
* @returns {AcceptStyleItem | undefined}
|
|
32
35
|
*/
|
|
33
|
-
static
|
|
36
|
+
static selectItemFrom(acceptEncodings, supportedTypes) {
|
|
37
|
+
if(acceptEncodings === undefined) { return undefined }
|
|
38
|
+
if(!Array.isArray(acceptEncodings)) { return undefined }
|
|
39
|
+
if(acceptEncodings.length === 0) { return undefined }
|
|
40
|
+
|
|
34
41
|
if(supportedTypes === undefined) { return undefined }
|
|
42
|
+
if(!Array.isArray(supportedTypes)) { return undefined }
|
|
43
|
+
if(supportedTypes.length === 0) { return undefined }
|
|
35
44
|
|
|
36
45
|
for(const acceptEncoding of acceptEncodings) {
|
|
37
46
|
const { name } = acceptEncoding
|
|
38
47
|
if(supportedTypes.includes(name)) {
|
|
39
|
-
return
|
|
48
|
+
return acceptEncoding
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
//
|
|
44
53
|
if(acceptEncodings.some(item => item.name === ENCODING_ANY)) {
|
|
45
|
-
|
|
54
|
+
const [ name ] = supportedTypes
|
|
55
|
+
if(name === undefined) { return undefined }
|
|
56
|
+
return { name }
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
return undefined
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated
|
|
64
|
+
* @see {@link AcceptEncoding.selectItemFrom}
|
|
65
|
+
* @param {Array<AcceptStyleItem>} acceptEncodings
|
|
66
|
+
* @param {Array<string>} supportedTypes
|
|
67
|
+
* @returns {string | undefined}
|
|
68
|
+
*/
|
|
69
|
+
static selectFrom(acceptEncodings, supportedTypes) {
|
|
70
|
+
const item = AcceptEncoding.selectItemFrom(acceptEncodings, supportedTypes)
|
|
71
|
+
return item?.name
|
|
72
|
+
}
|
|
50
73
|
}
|
|
@@ -19,6 +19,8 @@ export class AcceptLanguage {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
+
* @deprecated
|
|
23
|
+
* @see {@link AcceptLanguage.selectItemFrom}
|
|
22
24
|
* @param {string|undefined} acceptLanguageHeader
|
|
23
25
|
* @param {Array<string>} supportedTypes
|
|
24
26
|
*/
|
|
@@ -28,24 +30,46 @@ export class AcceptLanguage {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
* @param {Array<AcceptStyleItem>} acceptLanguages
|
|
32
|
-
* @param {Array<string>} supportedTypes
|
|
33
|
+
* @param {Array<AcceptStyleItem>} acceptLanguages (descending quality order)
|
|
34
|
+
* @param {Array<string>} supportedTypes (descending preferred order)
|
|
35
|
+
* @returns {AcceptStyleItem | undefined}
|
|
33
36
|
*/
|
|
34
|
-
static
|
|
37
|
+
static selectItemFrom(acceptLanguages, supportedTypes) {
|
|
38
|
+
if(acceptLanguages === undefined) { return undefined }
|
|
39
|
+
if(!Array.isArray(acceptLanguages)) { return undefined }
|
|
40
|
+
if(acceptLanguages.length === 0) { return undefined }
|
|
41
|
+
|
|
35
42
|
if(supportedTypes === undefined) { return undefined }
|
|
43
|
+
if(!Array.isArray(supportedTypes)) { return undefined }
|
|
44
|
+
if(supportedTypes.length === 0) { return undefined }
|
|
36
45
|
|
|
46
|
+
// this assume acceptLangues is quality sorted descending order
|
|
37
47
|
for(const acceptLanguage of acceptLanguages) {
|
|
38
48
|
const { name } = acceptLanguage
|
|
39
49
|
if(supportedTypes.includes(name)) {
|
|
40
|
-
return
|
|
50
|
+
return acceptLanguage
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
//
|
|
45
55
|
if(acceptLanguages.some(item => item.name === LANGUAGE_ANY)) {
|
|
46
|
-
|
|
56
|
+
const name = supportedTypes.at(0)
|
|
57
|
+
if(name === undefined) { return undefined }
|
|
58
|
+
return { name }
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
return undefined
|
|
50
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated
|
|
66
|
+
* @see {@link AcceptLanguage.selectItemFrom}
|
|
67
|
+
* @param {Array<AcceptStyleItem>} acceptLanguages
|
|
68
|
+
* @param {Array<string>} supportedTypes
|
|
69
|
+
* @returns {string | undefined}
|
|
70
|
+
*/
|
|
71
|
+
static selectFrom(acceptLanguages, supportedTypes) {
|
|
72
|
+
const item = AcceptLanguage.selectItemFrom(acceptLanguages, supportedTypes)
|
|
73
|
+
return item?.name
|
|
74
|
+
}
|
|
51
75
|
}
|
package/src/headers/accept.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseAcceptStyleHeader } from './util/accept-util.js'
|
|
2
|
-
import { MIME_ANY, Mime } from './util/mime.js'
|
|
2
|
+
import { MIME_ANY, MIME_SEPARATOR, Mime } from './util/mime.js'
|
|
3
3
|
|
|
4
4
|
/** @import { AcceptStyleItem } from './util/accept-util.js' */
|
|
5
5
|
/** @import { MimeItem } from './util/mime.js' */
|
|
@@ -11,7 +11,30 @@ export const WELL_KNOWN = new Map([
|
|
|
11
11
|
[ 'application/json', [ { name: 'application/json', quality: 1 } ] ]
|
|
12
12
|
])
|
|
13
13
|
|
|
14
|
+
export const UNSPECIFIED_QUALITY = 1
|
|
15
|
+
|
|
14
16
|
export class Accept {
|
|
17
|
+
/**
|
|
18
|
+
* Descending order based on quality and higher specificity.
|
|
19
|
+
*
|
|
20
|
+
* Returns negative is the first item (a) is of higher quality then second (b).
|
|
21
|
+
* @param {AcceptItem} a
|
|
22
|
+
* @param {AcceptItem} b
|
|
23
|
+
*/
|
|
24
|
+
static compare(a, b) {
|
|
25
|
+
if(a.quality === b.quality) {
|
|
26
|
+
// prefer things with less ANY
|
|
27
|
+
const specificityA = (a.type === MIME_ANY ? 1 : 0) + (a.subtype === MIME_ANY ? 1 : 0)
|
|
28
|
+
const specificityB = (b.type === MIME_ANY ? 1 : 0) + (b.subtype === MIME_ANY ? 1 : 0)
|
|
29
|
+
return Math.sign(specificityA - specificityB)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// B - A descending order
|
|
33
|
+
const qualityB = b.quality ?? UNSPECIFIED_QUALITY
|
|
34
|
+
const qualityA = a.quality ?? UNSPECIFIED_QUALITY
|
|
35
|
+
return Math.sign(qualityB - qualityA)
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
/**
|
|
16
39
|
* @param {string|undefined} acceptHeader
|
|
17
40
|
* @returns {Array<AcceptItem>}
|
|
@@ -31,22 +54,12 @@ export class Accept {
|
|
|
31
54
|
}
|
|
32
55
|
})
|
|
33
56
|
.filter(entry => entry !== undefined)
|
|
34
|
-
.sort(
|
|
35
|
-
if(entryA.quality === entryB.quality) {
|
|
36
|
-
// prefer things with less ANY
|
|
37
|
-
const specificityA = (entryA.type === MIME_ANY ? 1 : 0) + (entryA.subtype === MIME_ANY ? 1 : 0)
|
|
38
|
-
const specificityB = (entryB.type === MIME_ANY ? 1 : 0) + (entryB.subtype === MIME_ANY ? 1 : 0)
|
|
39
|
-
return specificityA - specificityB
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// B - A descending order
|
|
43
|
-
const qualityB = entryB.quality ?? 0
|
|
44
|
-
const qualityA = entryA.quality ?? 0
|
|
45
|
-
return qualityB - qualityA
|
|
46
|
-
})
|
|
57
|
+
.sort(Accept.compare)
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/**
|
|
61
|
+
* @deprecated
|
|
62
|
+
* @see {@link Accept.selectItemFrom}
|
|
50
63
|
* @param {string|undefined} acceptHeader
|
|
51
64
|
* @param {Array<string>} supportedTypes
|
|
52
65
|
*/
|
|
@@ -58,29 +71,61 @@ export class Accept {
|
|
|
58
71
|
/**
|
|
59
72
|
* @param {Array<AcceptItem>} accepts
|
|
60
73
|
* @param {Array<string>} supportedTypes
|
|
74
|
+
* @returns {AcceptItem | undefined}
|
|
61
75
|
*/
|
|
62
|
-
static
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
static selectItemFrom(accepts, supportedTypes) {
|
|
77
|
+
if(accepts === undefined) { return undefined }
|
|
78
|
+
if(!Array.isArray(accepts)) { return undefined }
|
|
79
|
+
if(accepts.length === 0) { return undefined }
|
|
80
|
+
|
|
81
|
+
if(supportedTypes === undefined) { return undefined }
|
|
82
|
+
if(!Array.isArray(supportedTypes)) { return undefined }
|
|
83
|
+
if(supportedTypes.length === 0) { return undefined }
|
|
84
|
+
|
|
85
|
+
const supportedMimeTypes = supportedTypes
|
|
86
|
+
.map(Mime.parse)
|
|
87
|
+
.filter(m => m !== undefined)
|
|
88
|
+
|
|
89
|
+
// todo if supportedMimeType has ANY show warning?
|
|
70
90
|
|
|
91
|
+
if(supportedMimeTypes.length === 0) { return undefined }
|
|
92
|
+
|
|
93
|
+
const matches = accepts.map(accept => {
|
|
94
|
+
const matchSupportedMimeTypes = supportedMimeTypes.filter(mt => Mime.matches(accept, mt))
|
|
95
|
+
const best = matchSupportedMimeTypes.at(0)
|
|
96
|
+
if(best === undefined) { return undefined }
|
|
97
|
+
|
|
98
|
+
// resolve mime type and subtype
|
|
99
|
+
const type = best.type === MIME_ANY ? accept.type : best.type
|
|
100
|
+
const subtype = best.subtype === MIME_ANY ? accept.subtype : best.subtype
|
|
101
|
+
|
|
102
|
+
// preserve name and parameters of source
|
|
71
103
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
...accept,
|
|
105
|
+
mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype}`,
|
|
106
|
+
type,
|
|
107
|
+
subtype
|
|
74
108
|
}
|
|
75
109
|
})
|
|
76
|
-
.filter(
|
|
110
|
+
.filter(m => m !== undefined)
|
|
77
111
|
|
|
78
|
-
if(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return firstSt
|
|
83
|
-
}
|
|
84
|
-
}
|
|
112
|
+
if(matches.length === 0) { return undefined }
|
|
113
|
+
|
|
114
|
+
// sort is in-place
|
|
115
|
+
matches.sort(Accept.compare)
|
|
85
116
|
|
|
117
|
+
return matches.at(0)
|
|
118
|
+
}
|
|
86
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @deprecated
|
|
122
|
+
* @see {@link Accept.selectItemFrom}
|
|
123
|
+
* @param {Array<AcceptItem>} accepts
|
|
124
|
+
* @param {Array<string>} supportedTypes
|
|
125
|
+
* @returns {string | undefined}
|
|
126
|
+
*/
|
|
127
|
+
static selectFrom(accepts, supportedTypes) {
|
|
128
|
+
const item = Accept.selectItemFrom(accepts, supportedTypes)
|
|
129
|
+
return item?.mimetype
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { COMMON_LIST_HEADER_JOINER_COMMA } from "../defs.js"
|
|
2
|
+
|
|
1
3
|
/** @typedef {'no-cache'|'no-store'|'no-transform'|'must-revalidate'|'immutable'|'must-understand'} Directives */
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -13,9 +15,10 @@
|
|
|
13
15
|
export class CacheControl {
|
|
14
16
|
/**
|
|
15
17
|
* @param {CacheControlOptions|undefined} options
|
|
16
|
-
* @
|
|
18
|
+
* @param {boolean} [asArray = false]
|
|
19
|
+
* @returns {Array<string>|string|undefined}
|
|
17
20
|
*/
|
|
18
|
-
static encode(options) {
|
|
21
|
+
static encode(options, asArray = false) {
|
|
19
22
|
if(options === undefined) { return undefined }
|
|
20
23
|
|
|
21
24
|
const {
|
|
@@ -51,6 +54,6 @@ export class CacheControl {
|
|
|
51
54
|
//
|
|
52
55
|
if(result.length === 0) { return undefined }
|
|
53
56
|
|
|
54
|
-
return result.join(
|
|
57
|
+
return asArray ? result : result.join(COMMON_LIST_HEADER_JOINER_COMMA)
|
|
55
58
|
}
|
|
56
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { COMMON_LIST_HEADER_JOINER_COMMA } from "../defs.js"
|
|
2
2
|
|
|
3
3
|
export const CLIENT_HINT_USER_AGENT = 'Sec-CH-UA'
|
|
4
4
|
export const CLIENT_HINT_ARCHITECTURE = 'Sec-CH-UA-Arch'
|
|
@@ -69,8 +69,9 @@ export class ClientHints {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* @param {Array<String>} hints
|
|
72
|
+
* @param {boolean} [asArray = false]
|
|
72
73
|
*/
|
|
73
|
-
static encode(hints) {
|
|
74
|
+
static encode(hints, asArray = false) {
|
|
74
75
|
if(hints === undefined) { return undefined }
|
|
75
76
|
if(!Array.isArray(hints)) { return undefined }
|
|
76
77
|
if(hints.length === 0) { return undefined }
|
|
@@ -80,7 +81,7 @@ export class ClientHints {
|
|
|
80
81
|
|
|
81
82
|
if(remaining.length === 0) { return undefined }
|
|
82
83
|
|
|
83
|
-
return remaining.join(
|
|
84
|
+
return asArray ? remaining : remaining.join(COMMON_LIST_HEADER_JOINER_COMMA)
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -102,7 +102,7 @@ export class ETag {
|
|
|
102
102
|
static isValid(etag) {
|
|
103
103
|
if(etag === undefined) { return false }
|
|
104
104
|
|
|
105
|
-
// %x21 / %x23-7E
|
|
105
|
+
// %x21 / %x23-7E and %x80-FF
|
|
106
106
|
for(const c of etag) {
|
|
107
107
|
if(c.charCodeAt(0) < 0x21) { return false }
|
|
108
108
|
if(c.charCodeAt(0) > 0xFF) { return false }
|
|
@@ -143,7 +143,7 @@ export class ETag {
|
|
|
143
143
|
|
|
144
144
|
const rawEtag = raw.trim()
|
|
145
145
|
const weak = rawEtag.startsWith(CONDITION_ETAG_WEAK_PREFIX)
|
|
146
|
-
const quotedEtag = weak ? rawEtag.
|
|
146
|
+
const quotedEtag = weak ? rawEtag.slice(CONDITION_ETAG_WEAK_PREFIX.length) : rawEtag
|
|
147
147
|
|
|
148
148
|
if(quotedEtag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
|
|
149
149
|
|
|
@@ -350,18 +350,18 @@ export class Conditional {
|
|
|
350
350
|
|
|
351
351
|
//
|
|
352
352
|
const spaces = [
|
|
353
|
-
matchHeader.
|
|
354
|
-
matchHeader.
|
|
355
|
-
matchHeader.
|
|
356
|
-
matchHeader.
|
|
357
|
-
matchHeader.
|
|
353
|
+
matchHeader.slice(4, 5),
|
|
354
|
+
matchHeader.slice(7, 8),
|
|
355
|
+
matchHeader.slice(11, 12),
|
|
356
|
+
matchHeader.slice(16, 17),
|
|
357
|
+
matchHeader.slice(25, 26)
|
|
358
358
|
]
|
|
359
|
-
const comma = matchHeader.
|
|
359
|
+
const comma = matchHeader.slice(3, 4)
|
|
360
360
|
const timeSeparators = [
|
|
361
|
-
matchHeader.
|
|
362
|
-
matchHeader.
|
|
361
|
+
matchHeader.slice(19, 20),
|
|
362
|
+
matchHeader.slice(22, 23)
|
|
363
363
|
]
|
|
364
|
-
const gmt = matchHeader.
|
|
364
|
+
const gmt = matchHeader.slice(26)
|
|
365
365
|
|
|
366
366
|
//
|
|
367
367
|
if(comma !== DATE_SEPARATOR) { return undefined }
|
|
@@ -374,13 +374,13 @@ export class Conditional {
|
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
//
|
|
377
|
-
const dayName = matchHeader.
|
|
378
|
-
const day = Number.parseInt(matchHeader.
|
|
379
|
-
const month = matchHeader.
|
|
380
|
-
const year = Number.parseInt(matchHeader.
|
|
381
|
-
const hour = Number.parseInt(matchHeader.
|
|
382
|
-
const minute = Number.parseInt(matchHeader.
|
|
383
|
-
const second = Number.parseInt(matchHeader.
|
|
377
|
+
const dayName = matchHeader.slice(0, 3)
|
|
378
|
+
const day = Number.parseInt(matchHeader.slice(5, 7))
|
|
379
|
+
const month = matchHeader.slice(8, 11)
|
|
380
|
+
const year = Number.parseInt(matchHeader.slice(12, 16))
|
|
381
|
+
const hour = Number.parseInt(matchHeader.slice(17, 19))
|
|
382
|
+
const minute = Number.parseInt(matchHeader.slice(20, 22))
|
|
383
|
+
const second = Number.parseInt(matchHeader.slice(23, 25))
|
|
384
384
|
|
|
385
385
|
//
|
|
386
386
|
if(!DATE_DAYS.includes(dayName)) { return undefined }
|
package/src/headers/link.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { COMMON_LIST_HEADER_JOINER_COMMA } from "../defs.js"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {Object} LinkItem
|
|
3
5
|
* @property {URL|string} url
|
|
@@ -9,7 +11,7 @@ export class Link {
|
|
|
9
11
|
/**
|
|
10
12
|
* @param {LinkItem} link
|
|
11
13
|
*/
|
|
12
|
-
static
|
|
14
|
+
static *#encode(link) {
|
|
13
15
|
const encodedUri = (link.url instanceof URL) ? link.url : encodeURI(link.url)
|
|
14
16
|
|
|
15
17
|
yield `<${encodedUri}>`
|
|
@@ -22,13 +24,16 @@ export class Link {
|
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* @param {Array<LinkItem>|LinkItem|undefined} links
|
|
27
|
+
* @param {boolean} [asArray = false]
|
|
25
28
|
*/
|
|
26
|
-
static encode(links) {
|
|
29
|
+
static encode(links, asArray = false) {
|
|
27
30
|
if(links === undefined) { return undefined }
|
|
28
31
|
const linkAry = Array.isArray(links) ? links : [ links ]
|
|
29
32
|
if(linkAry.length === 0) { return undefined }
|
|
30
|
-
|
|
33
|
+
|
|
34
|
+
const ary = linkAry
|
|
31
35
|
.map(link => [ ...Link.#encode(link) ].join('; '))
|
|
32
|
-
|
|
36
|
+
|
|
37
|
+
return asArray ? ary : ary.join(COMMON_LIST_HEADER_JOINER_COMMA)
|
|
33
38
|
}
|
|
34
39
|
}
|
package/src/headers/multipart.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { ReadableStream } from 'node:stream/web'
|
|
2
2
|
|
|
3
|
-
import { isQuoted, stripQuotes } from '../headers/util/quote.js'
|
|
4
3
|
import { ContentDisposition } from './content-disposition.js'
|
|
5
4
|
import { ContentRange } from './content-range.js'
|
|
6
|
-
import { ContentType } from './content-type.js'
|
|
5
|
+
// import { ContentType } from './content-type.js'
|
|
7
6
|
|
|
8
7
|
/** @import { ContentRangeDirective } from './content-range.js' */
|
|
9
8
|
/** @import { SendBody } from '../defs.js' */
|
|
@@ -65,11 +64,6 @@ export class Multipart {
|
|
|
65
64
|
|
|
66
65
|
const lines = text.split(MULTIPART_SEPARATOR)
|
|
67
66
|
|
|
68
|
-
if(lines.length === 0) {
|
|
69
|
-
// missing body?
|
|
70
|
-
return formData
|
|
71
|
-
}
|
|
72
|
-
|
|
73
67
|
const boundaryBegin = `${BOUNDARY_MARK}${boundary}`
|
|
74
68
|
const boundaryEnd = `${BOUNDARY_MARK}${boundary}${BOUNDARY_MARK}`
|
|
75
69
|
|
|
@@ -98,7 +92,7 @@ export class Multipart {
|
|
|
98
92
|
const name = rawName?.toLowerCase()
|
|
99
93
|
// console.log('header', name, value)
|
|
100
94
|
if(name === MULTIPART_HEADER.CONTENT_TYPE) {
|
|
101
|
-
const contentType = ContentType.parse(value)
|
|
95
|
+
//const contentType = ContentType.parse(value)
|
|
102
96
|
//console.log({ contentType })
|
|
103
97
|
}
|
|
104
98
|
else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
|
|
@@ -107,7 +101,10 @@ export class Multipart {
|
|
|
107
101
|
throw new Error('disposition not form-data')
|
|
108
102
|
}
|
|
109
103
|
|
|
110
|
-
partName =
|
|
104
|
+
partName = disposition.name
|
|
105
|
+
}
|
|
106
|
+
else if(name === MULTIPART_HEADER.CONTENT_RANGE) {
|
|
107
|
+
// todo
|
|
111
108
|
}
|
|
112
109
|
else {
|
|
113
110
|
// unsupported part header - ignore
|
|
@@ -131,10 +128,9 @@ export class Multipart {
|
|
|
131
128
|
}
|
|
132
129
|
state = MULTIPART_STATE.HEADERS
|
|
133
130
|
}
|
|
134
|
-
else {
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
131
|
+
// else {
|
|
132
|
+
// throw new Error('unknown state')
|
|
133
|
+
// }
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
return formData
|
|
@@ -145,7 +141,7 @@ export class Multipart {
|
|
|
145
141
|
* @param {Array<MultipartBytePart>} parts
|
|
146
142
|
* @param {number|undefined} contentLength
|
|
147
143
|
* @param {string} boundary
|
|
148
|
-
* @returns {ReadableStream<Uint8Array
|
|
144
|
+
* @returns {ReadableStream<Uint8Array<ArrayBuffer>>}
|
|
149
145
|
*/
|
|
150
146
|
static encode_Bytes(contentType, parts, contentLength, boundary) {
|
|
151
147
|
const boundaryBegin = `${BOUNDARY_MARK}${boundary}`
|
|
@@ -166,16 +162,21 @@ export class Multipart {
|
|
|
166
162
|
if(part.obj instanceof ReadableStream) {
|
|
167
163
|
// biome-ignore lint/performance/noAwaitInLoops: readable
|
|
168
164
|
for await (const chunk of part.obj) {
|
|
169
|
-
if(chunk instanceof ArrayBuffer
|
|
165
|
+
if(chunk instanceof ArrayBuffer) {
|
|
166
|
+
controller.enqueue(new Uint8Array(chunk))
|
|
167
|
+
}
|
|
168
|
+
else if(ArrayBuffer.isView(chunk)) {
|
|
170
169
|
controller.enqueue(chunk)
|
|
171
170
|
}
|
|
172
171
|
else if(typeof chunk === 'string'){
|
|
173
172
|
controller.enqueue(encoder.encode(chunk))
|
|
174
173
|
}
|
|
175
|
-
else {
|
|
176
|
-
// console.log('chunk type', typeof chunk)
|
|
174
|
+
else if(typeof chunk === 'number') {
|
|
177
175
|
controller.enqueue(Uint8Array.from([ chunk ]))
|
|
178
176
|
}
|
|
177
|
+
else {
|
|
178
|
+
controller.error(new Error('unknown stream chunk type'))
|
|
179
|
+
}
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
else if(part.obj instanceof ArrayBuffer) {
|