@johntalton/http-util 5.0.0 → 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.
- package/package.json +1 -1
- package/src/accept-encoding.js +1 -1
- package/src/accept-language.js +1 -1
- package/src/accept-util.js +1 -1
- package/src/accept.js +2 -5
- package/src/body.js +6 -5
- package/src/clear-site-data.js +59 -0
- package/src/conditional.js +11 -8
- package/src/content-disposition.js +12 -7
- package/src/content-type.js +15 -14
- package/src/forwarded.js +1 -1
- package/src/index.js +3 -1
- package/src/multipart.js +5 -5
- package/src/preference.js +205 -0
- package/src/range.js +5 -10
- package/src/response/accepted.js +1 -0
- package/src/response/bytes.js +1 -0
- package/src/response/conflict.js +1 -0
- package/src/response/content-too-large.js +1 -0
- package/src/response/created.js +2 -1
- package/src/response/defs.js +2 -0
- package/src/response/error.js +1 -0
- package/src/response/gone.js +1 -0
- package/src/response/header-util.js +2 -1
- package/src/response/im-a-teapot.js +1 -0
- package/src/response/index.js +3 -0
- package/src/response/insufficient-storage.js +1 -0
- package/src/response/json.js +1 -1
- package/src/response/moved-permanently.js +1 -0
- package/src/response/multiple-choices.js +1 -0
- package/src/response/no-content.js +2 -1
- package/src/response/not-acceptable.js +2 -1
- package/src/response/not-allowed.js +1 -0
- package/src/response/not-found.js +1 -0
- package/src/response/not-implemented.js +1 -0
- package/src/response/not-modified.js +3 -2
- package/src/response/partial-content.js +3 -3
- package/src/response/precondition-failed.js +1 -0
- package/src/response/preflight.js +2 -1
- package/src/response/range-not-satisfiable.js +2 -1
- package/src/response/see-other.js +1 -0
- package/src/response/send-util.js +14 -8
- package/src/response/sse.js +4 -3
- package/src/response/temporary-redirect.js +1 -0
- package/src/response/timeout.js +1 -0
- package/src/response/too-many-requests.js +1 -0
- package/src/response/trace.js +8 -4
- package/src/response/unauthorized.js +1 -0
- package/src/response/unavailable.js +1 -0
- package/src/response/unprocessable.js +1 -0
- package/src/response/unsupported-media.js +10 -4
package/package.json
CHANGED
package/src/accept-encoding.js
CHANGED
|
@@ -21,7 +21,7 @@ export class AcceptEncoding {
|
|
|
21
21
|
*/
|
|
22
22
|
static select(acceptEncodingHeader, supportedTypes) {
|
|
23
23
|
const accepts = AcceptEncoding.parse(acceptEncodingHeader)
|
|
24
|
-
return
|
|
24
|
+
return AcceptEncoding.selectFrom(accepts, supportedTypes)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
package/src/accept-language.js
CHANGED
|
@@ -23,7 +23,7 @@ export class AcceptLanguage {
|
|
|
23
23
|
*/
|
|
24
24
|
static select(acceptLanguageHeader, supportedTypes) {
|
|
25
25
|
const accepts = AcceptLanguage.parse(acceptLanguageHeader)
|
|
26
|
-
return
|
|
26
|
+
return AcceptLanguage.selectFrom(accepts, supportedTypes)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
package/src/accept-util.js
CHANGED
|
@@ -39,7 +39,7 @@ export function parseAcceptStyleHeader(header, wellKnown) {
|
|
|
39
39
|
}))
|
|
40
40
|
|
|
41
41
|
if(!parameters.has(QUALITY)) { parameters.set(QUALITY, DEFAULT_QUALITY_STRING) }
|
|
42
|
-
const quality = parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
|
|
42
|
+
const quality = Number.parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
45
|
name,
|
package/src/accept.js
CHANGED
|
@@ -54,7 +54,6 @@ export class Accept {
|
|
|
54
54
|
const qualityB = entryB.quality ?? 0
|
|
55
55
|
const qualityA = entryA.quality ?? 0
|
|
56
56
|
return qualityB - qualityA
|
|
57
|
-
// return entryB.quality - entryA.quality
|
|
58
57
|
})
|
|
59
58
|
}
|
|
60
59
|
|
|
@@ -64,7 +63,7 @@ export class Accept {
|
|
|
64
63
|
*/
|
|
65
64
|
static select(acceptHeader, supportedTypes) {
|
|
66
65
|
const accepts = Accept.parse(acceptHeader)
|
|
67
|
-
return
|
|
66
|
+
return Accept.selectFrom(accepts, supportedTypes)
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/**
|
|
@@ -84,9 +83,7 @@ export class Accept {
|
|
|
84
83
|
quality
|
|
85
84
|
}
|
|
86
85
|
})
|
|
87
|
-
.filter(best =>
|
|
88
|
-
return best.supportedTypes.length > 0
|
|
89
|
-
})
|
|
86
|
+
.filter(best => best.supportedTypes.length > 0)
|
|
90
87
|
|
|
91
88
|
if(bests.length === 0) { return undefined }
|
|
92
89
|
const [ first ] = bests
|
package/src/body.js
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
} from './content-type.js'
|
|
6
6
|
import { Multipart } from './multipart.js'
|
|
7
7
|
|
|
8
|
-
export const
|
|
8
|
+
export const BYTE_PER_K = 1024
|
|
9
|
+
export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
|
|
9
10
|
|
|
10
11
|
/** @import { Readable } from 'node:stream' */
|
|
11
12
|
/** @import { ContentType } from './content-type.js' */
|
|
@@ -43,7 +44,7 @@ export function requestBody(stream, options) {
|
|
|
43
44
|
const charset = options?.contentType?.charset ?? CHARSET_UTF8
|
|
44
45
|
const contentType = options?.contentType
|
|
45
46
|
|
|
46
|
-
const invalidContentLength = (contentLength === undefined || isNaN(contentLength))
|
|
47
|
+
const invalidContentLength = (contentLength === undefined || Number.isNaN(contentLength))
|
|
47
48
|
// if(contentLength > byteLimit) {
|
|
48
49
|
// console.log(contentLength, invalidContentLength)
|
|
49
50
|
// throw new Error('contentLength exceeds limit')
|
|
@@ -278,7 +279,7 @@ export async function bodyJSON(reader, charset) {
|
|
|
278
279
|
* @param {ReadableStream} reader
|
|
279
280
|
* @param {ContentType} contentType
|
|
280
281
|
*/
|
|
281
|
-
async function _bodyFormData_Multipart(reader, contentType) {
|
|
282
|
+
export async function _bodyFormData_Multipart(reader, contentType) {
|
|
282
283
|
const MULTIPART_FORM_DATA_BOUNDARY_PARAMETER = 'boundary'
|
|
283
284
|
|
|
284
285
|
const text = await bodyText(reader, contentType.charset)
|
|
@@ -292,7 +293,7 @@ async function _bodyFormData_Multipart(reader, contentType) {
|
|
|
292
293
|
* @param {ReadableStream} reader
|
|
293
294
|
* @param {ContentType} contentType
|
|
294
295
|
*/
|
|
295
|
-
async function _bodyFormData_URL(reader, contentType) {
|
|
296
|
+
export async function _bodyFormData_URL(reader, contentType) {
|
|
296
297
|
const text = await bodyText(reader, contentType.charset)
|
|
297
298
|
const sp = new URLSearchParams(text)
|
|
298
299
|
const formData = new FormData()
|
|
@@ -320,4 +321,4 @@ export async function bodyFormData(reader, contentType) {
|
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
throw new TypeError('unknown mime type for form data')
|
|
323
|
-
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
/** @type {'*'} */
|
|
3
|
+
export const WILDCARD = '*'
|
|
4
|
+
|
|
5
|
+
export const CSD_DIRECTIVE_SEPARATOR = ','
|
|
6
|
+
export const CSD_QUOTE = '"'
|
|
7
|
+
|
|
8
|
+
export const CSD_DIRECTIVE_CACHE = 'cache'
|
|
9
|
+
export const CSD_DIRECTIVE_CLIENT_HINTS = 'clientHints'
|
|
10
|
+
export const CSD_DIRECTIVE_COOKIES = 'cookies'
|
|
11
|
+
export const CSD_DIRECTIVE_EXECUTION_CONTEXTS = 'executionContexts'
|
|
12
|
+
export const CSD_DIRECTIVE_PREFETCH_CACHE = 'prefetchCache'
|
|
13
|
+
export const CSD_DIRECTIVE_PRERENDER_CACHE = 'prerenderCache'
|
|
14
|
+
export const CSD_DIRECTIVE_STORAGE = 'storage'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} ClearOptions
|
|
18
|
+
* @property {boolean} cache
|
|
19
|
+
* @property {boolean} clientHints
|
|
20
|
+
* @property {boolean} cookies
|
|
21
|
+
* @property {boolean} executionContext
|
|
22
|
+
* @property {boolean} prefetchCache
|
|
23
|
+
* @property {boolean} prerenderCache
|
|
24
|
+
* @property {boolean} storage
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export class SiteData {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Partial<ClearOptions>|true|'*'|undefined} directives
|
|
30
|
+
*/
|
|
31
|
+
static encode(directives) {
|
|
32
|
+
if(directives === undefined) { return undefined }
|
|
33
|
+
if(directives === true) { return WILDCARD }
|
|
34
|
+
if(directives === WILDCARD) { return WILDCARD }
|
|
35
|
+
|
|
36
|
+
const result = []
|
|
37
|
+
if(directives.cache === true) { result.push(CSD_DIRECTIVE_CACHE) }
|
|
38
|
+
if(directives.clientHints === true) { result.push(CSD_DIRECTIVE_CLIENT_HINTS) }
|
|
39
|
+
if(directives.cookies === true) { result.push(CSD_DIRECTIVE_COOKIES) }
|
|
40
|
+
if(directives.executionContext === true) { result.push(CSD_DIRECTIVE_EXECUTION_CONTEXTS) }
|
|
41
|
+
if(directives.prefetchCache === true) { result.push(CSD_DIRECTIVE_PREFETCH_CACHE) }
|
|
42
|
+
if(directives.prerenderCache === true) { result.push(CSD_DIRECTIVE_PRERENDER_CACHE) }
|
|
43
|
+
if(directives.storage === true) { result.push(CSD_DIRECTIVE_STORAGE) }
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
.map(item => `${CSD_QUOTE}${item}${CSD_QUOTE}`)
|
|
47
|
+
.join(CSD_DIRECTIVE_SEPARATOR)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// console.log(SiteData.encode())
|
|
52
|
+
// console.log(SiteData.encode({}))
|
|
53
|
+
// console.log(SiteData.encode(false))
|
|
54
|
+
// console.log(SiteData.encode('true'))
|
|
55
|
+
// console.log(SiteData.encode(true))
|
|
56
|
+
// console.log(SiteData.encode('*'))
|
|
57
|
+
// console.log(SiteData.encode({ storage: false }))
|
|
58
|
+
// console.log(SiteData.encode({ storage: true }))
|
|
59
|
+
// console.log(SiteData.encode({ storage: true, cookies: true }))
|
package/src/conditional.js
CHANGED
|
@@ -49,6 +49,9 @@ export const DATE_SEPARATOR = ','
|
|
|
49
49
|
export const DATE_TIME_SEPARATOR = ':'
|
|
50
50
|
export const DATE_ZONE = 'GMT'
|
|
51
51
|
|
|
52
|
+
export const MINIMUM_YEAR = 1900
|
|
53
|
+
export const MAXIMUM_DAY = 31
|
|
54
|
+
|
|
52
55
|
/**
|
|
53
56
|
* @param {string} etag
|
|
54
57
|
*/
|
|
@@ -201,7 +204,7 @@ export class Conditional {
|
|
|
201
204
|
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
|
202
205
|
// day-name "," SP date1 SP time-of-day SP GMT
|
|
203
206
|
|
|
204
|
-
if(matchHeader.length
|
|
207
|
+
if(matchHeader.length !== 29) { return undefined }
|
|
205
208
|
|
|
206
209
|
//
|
|
207
210
|
const spaces = [
|
|
@@ -230,12 +233,12 @@ export class Conditional {
|
|
|
230
233
|
|
|
231
234
|
//
|
|
232
235
|
const dayName = matchHeader.substring(0, 3)
|
|
233
|
-
const day = parseInt(matchHeader.substring(5, 7))
|
|
236
|
+
const day = Number.parseInt(matchHeader.substring(5, 7))
|
|
234
237
|
const month = matchHeader.substring(8, 11)
|
|
235
|
-
const year = parseInt(matchHeader.substring(12, 16))
|
|
236
|
-
const hour = parseInt(matchHeader.substring(17, 19))
|
|
237
|
-
const minute = parseInt(matchHeader.substring(20, 22))
|
|
238
|
-
const second = parseInt(matchHeader.substring(23, 25))
|
|
238
|
+
const year = Number.parseInt(matchHeader.substring(12, 16))
|
|
239
|
+
const hour = Number.parseInt(matchHeader.substring(17, 19))
|
|
240
|
+
const minute = Number.parseInt(matchHeader.substring(20, 22))
|
|
241
|
+
const second = Number.parseInt(matchHeader.substring(23, 25))
|
|
239
242
|
|
|
240
243
|
//
|
|
241
244
|
if(!DATE_DAYS.includes(dayName)) { return undefined }
|
|
@@ -247,8 +250,8 @@ export class Conditional {
|
|
|
247
250
|
if(!Number.isInteger(second)) { return undefined }
|
|
248
251
|
|
|
249
252
|
//
|
|
250
|
-
if(day >
|
|
251
|
-
if(year <
|
|
253
|
+
if(day > MAXIMUM_DAY || day <= 0) { return undefined }
|
|
254
|
+
if(year < MINIMUM_YEAR) { return undefined }
|
|
252
255
|
if(hour > 24 || hour < 0) { return undefined }
|
|
253
256
|
if(minute > 60 || minute < 0) { return undefined }
|
|
254
257
|
if(second > 60 || second < 0) { return undefined }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} Disposition
|
|
3
3
|
* @property {string} disposition
|
|
4
|
-
* @property {Map<string, string>} parameters
|
|
5
|
-
* @property {string} [name]
|
|
6
|
-
* @property {string} [filename]
|
|
4
|
+
* @property {Map<string, string|undefined>} parameters
|
|
5
|
+
* @property {string|undefined} [name]
|
|
6
|
+
* @property {string|undefined} [filename]
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const DISPOSITION_SEPARATOR = {
|
|
@@ -15,17 +15,22 @@ export const DISPOSITION_PARAM_NAME = 'name'
|
|
|
15
15
|
export const DISPOSITION_PARAM_FILENAME = 'filename'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* @param {string} contentDispositionHeader
|
|
18
|
+
* @param {string|undefined} contentDispositionHeader
|
|
19
19
|
* @returns {Disposition|undefined}
|
|
20
20
|
*/
|
|
21
21
|
export function parseContentDisposition(contentDispositionHeader) {
|
|
22
22
|
if(contentDispositionHeader === undefined) { return undefined }
|
|
23
23
|
|
|
24
24
|
const [ disposition, ...parameterSet ] = contentDispositionHeader.trim().split(DISPOSITION_SEPARATOR.PARAMETER).map(entry => entry.trim())
|
|
25
|
+
if(disposition === undefined) { return undefined }
|
|
25
26
|
const parameters = new Map(parameterSet.map(parameter => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
|
|
28
|
+
if(key === undefined) { return undefined }
|
|
29
|
+
return { key, value }
|
|
30
|
+
})
|
|
31
|
+
.filter(item => item !== undefined)
|
|
32
|
+
.map(({ key, value }) => ([ key, value ])))
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
const name = parameters.get(DISPOSITION_PARAM_NAME)
|
|
31
36
|
const filename = parameters.get(DISPOSITION_PARAM_FILENAME)
|
package/src/content-type.js
CHANGED
|
@@ -29,10 +29,12 @@ export const SPECIAL_CHARS = [
|
|
|
29
29
|
'\n', '\r', '\t'
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
+
export const WHITESPACE_REGEX = /\s/
|
|
33
|
+
|
|
32
34
|
/**
|
|
33
35
|
* @param {string} c
|
|
34
36
|
*/
|
|
35
|
-
export function isWhitespace(c){ return
|
|
37
|
+
export function isWhitespace(c){ return WHITESPACE_REGEX.test(c) }
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* @param {string|undefined} value
|
|
@@ -116,22 +118,21 @@ export function parseContentType(contentTypeHeader) {
|
|
|
116
118
|
|
|
117
119
|
const parameters = new Map()
|
|
118
120
|
|
|
119
|
-
parameterSet
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if(hasSpecialChar(key)) { return }
|
|
121
|
+
for(const parameter of parameterSet) {
|
|
122
|
+
const [ key, value ] = parameter.split(CONTENT_TYPE_SEPARATOR.KVP)
|
|
123
|
+
if(key === undefined || key === '') { return }
|
|
124
|
+
if(value === undefined || value === '') { return }
|
|
125
|
+
if(hasSpecialChar(key)) { return }
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
const actualKey = key?.trim().toLowerCase()
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
const quoted = (value.at(0) === '"' && value.at(-1) === '"')
|
|
130
|
+
const actualValue = quoted ? value.substring(1, value.length - 1) : value
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
if(!parameters.has(actualKey)) {
|
|
133
|
+
parameters.set(actualKey, actualValue)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
135
136
|
|
|
136
137
|
const charset = parameters.get(CHARSET)
|
|
137
138
|
|
package/src/forwarded.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
/** biome-ignore-all lint/performance/noBarrelFile: entry point */
|
|
2
|
+
/** biome-ignore-all lint/performance/noReExportAll: entry point */
|
|
3
|
+
export * from './accept.js'
|
|
1
4
|
export * from './accept-encoding.js'
|
|
2
5
|
export * from './accept-language.js'
|
|
3
6
|
export * from './accept-util.js'
|
|
4
|
-
export * from './accept.js'
|
|
5
7
|
export * from './cache-control.js'
|
|
6
8
|
export * from './conditional.js'
|
|
7
9
|
export * from './content-disposition.js'
|
package/src/multipart.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ReadableStream } from 'node:stream/web'
|
|
2
2
|
|
|
3
3
|
import { parseContentDisposition } from './content-disposition.js'
|
|
4
|
-
import { parseContentType } from './content-type.js'
|
|
5
4
|
import { ContentRange } from './content-range.js'
|
|
5
|
+
import { parseContentType } from './content-type.js'
|
|
6
6
|
|
|
7
7
|
/** @import { ContentRangeDirective } from './content-range.js' */
|
|
8
8
|
/** @import { SendBody } from './response/send-util.js' */
|
|
@@ -48,9 +48,9 @@ export class Multipart {
|
|
|
48
48
|
/**
|
|
49
49
|
* @param {string} text
|
|
50
50
|
* @param {string} boundary
|
|
51
|
-
* @param {string} [
|
|
51
|
+
* @param {string} [_charset='utf8']
|
|
52
52
|
*/
|
|
53
|
-
static parse_FormData(text, boundary,
|
|
53
|
+
static parse_FormData(text, boundary, _charset = 'utf8') {
|
|
54
54
|
// console.log({ boundary, text })
|
|
55
55
|
const formData = new FormData()
|
|
56
56
|
|
|
@@ -91,10 +91,10 @@ export class Multipart {
|
|
|
91
91
|
if(line === EMPTY) { state = MULTIPART_STATE.VALUE }
|
|
92
92
|
else {
|
|
93
93
|
const [ rawName, value ] = line.split(HEADER_SEPARATOR)
|
|
94
|
-
const name = rawName
|
|
94
|
+
const name = rawName?.toLowerCase()
|
|
95
95
|
// console.log('header', name, value)
|
|
96
96
|
if(name === MULTIPART_HEADER.CONTENT_TYPE) {
|
|
97
|
-
const
|
|
97
|
+
const _contentType = parseContentType(value)
|
|
98
98
|
// console.log({ contentType })
|
|
99
99
|
}
|
|
100
100
|
else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// https://datatracker.ietf.org/doc/html/rfc7240
|
|
2
|
+
// https://www.rfc-editor.org/rfc/rfc7240#section-3
|
|
3
|
+
|
|
4
|
+
export const SEPARATOR = {
|
|
5
|
+
PREFERENCE: ',',
|
|
6
|
+
PARAMS: ';',
|
|
7
|
+
PARAM_KVP: '=',
|
|
8
|
+
KVP: '='
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const QUOTE = '"'
|
|
12
|
+
|
|
13
|
+
export const DIRECTIVE_RESPOND_ASYNC = 'respond-async'
|
|
14
|
+
export const DIRECTIVE_WAIT = 'wait'
|
|
15
|
+
export const DIRECTIVE_HANDLING = 'handling'
|
|
16
|
+
export const DIRECTIVE_REPRESENTATION = 'return'
|
|
17
|
+
export const DIRECTIVE_TIMEZONE = 'timezone'
|
|
18
|
+
|
|
19
|
+
export const DIRECTIVE_HANDLING_STRICT = 'strict'
|
|
20
|
+
export const DIRECTIVE_HANDLING_LENIENT = 'lenient'
|
|
21
|
+
|
|
22
|
+
export const DIRECTIVE_REPRESENTATION_MINIMAL = 'minimal'
|
|
23
|
+
export const DIRECTIVE_REPRESENTATION_HEADERS_ONLY = 'headers-only'
|
|
24
|
+
export const DIRECTIVE_REPRESENTATION_FULL = 'representation'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string|undefined} value
|
|
29
|
+
*/
|
|
30
|
+
export function stripQuotes(value) {
|
|
31
|
+
if(value === undefined) { return undefined }
|
|
32
|
+
return value.substring(1, value.length - 1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string|undefined} value
|
|
37
|
+
*/
|
|
38
|
+
export function isQuoted(value) {
|
|
39
|
+
if(value === undefined) { return false }
|
|
40
|
+
if(value.length < 2) { return false }
|
|
41
|
+
if(!value.startsWith(QUOTE)) { return false }
|
|
42
|
+
if(!value.endsWith(QUOTE)) { return false }
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} Preference
|
|
48
|
+
* @property {string|undefined} value
|
|
49
|
+
* @property {Map<string, string|undefined>} parameters
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @template P
|
|
54
|
+
* @typedef {Object} RequestPreferencesBase
|
|
55
|
+
* @property {boolean|undefined} [asynchronous]
|
|
56
|
+
* @property {string|undefined} [representation]
|
|
57
|
+
* @property {string|undefined} [handling]
|
|
58
|
+
* @property {number|undefined} [wait]
|
|
59
|
+
* @property {string|undefined} [timezone]
|
|
60
|
+
* @property {Map<string, P>} preferences
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** @typedef {RequestPreferencesBase<Preference>} RequestPreferences */
|
|
64
|
+
/** @typedef {RequestPreferencesBase<Partial<Omit<Preference,'parameters'>>|undefined>} AppliedRequestPreferences */
|
|
65
|
+
|
|
66
|
+
export class Preferences {
|
|
67
|
+
/**
|
|
68
|
+
* @param {string|undefined} header
|
|
69
|
+
* @returns {RequestPreferences|undefined}
|
|
70
|
+
*/
|
|
71
|
+
static parse(header) {
|
|
72
|
+
if(header === undefined) { return undefined }
|
|
73
|
+
|
|
74
|
+
const preferences = new Map(header.split(SEPARATOR.PREFERENCE)
|
|
75
|
+
.map(pref => {
|
|
76
|
+
const [ kvp, ...params ] = pref.trim().split(SEPARATOR.PARAMS)
|
|
77
|
+
const [ key, rawValue ] = kvp?.split(SEPARATOR.KVP) ?? []
|
|
78
|
+
|
|
79
|
+
if(key === undefined) { return {} }
|
|
80
|
+
const valueOrEmpty = isQuoted(rawValue) ? stripQuotes(rawValue) : rawValue
|
|
81
|
+
const value = (valueOrEmpty !== '') ? valueOrEmpty : undefined
|
|
82
|
+
|
|
83
|
+
const parameters = new Map(params
|
|
84
|
+
.map(param => {
|
|
85
|
+
const [ pKey, rawPValue ] = param.split(SEPARATOR.PARAM_KVP)
|
|
86
|
+
if(pKey === undefined) { return {} }
|
|
87
|
+
const trimmedRawPValue = rawPValue?.trim()
|
|
88
|
+
const pValueOrEmpty = isQuoted(trimmedRawPValue) ? stripQuotes(trimmedRawPValue) : trimmedRawPValue
|
|
89
|
+
const pValue = (pValueOrEmpty !== '') ? pValueOrEmpty : undefined
|
|
90
|
+
return { key: pKey.trim(), value: pValue }
|
|
91
|
+
})
|
|
92
|
+
.filter(item => item.key !== undefined)
|
|
93
|
+
.map(item => ([ item.key, item.value ]))
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return { key, value, parameters }
|
|
97
|
+
})
|
|
98
|
+
.filter(item => item.key !== undefined)
|
|
99
|
+
.map(item => ([
|
|
100
|
+
item.key,
|
|
101
|
+
{ value: item.value, parameters: item.parameters }
|
|
102
|
+
]))
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
//
|
|
106
|
+
const asynchronous = preferences.get(DIRECTIVE_RESPOND_ASYNC) !== undefined
|
|
107
|
+
const representation = preferences.get(DIRECTIVE_REPRESENTATION)?.value
|
|
108
|
+
const handling = preferences.get(DIRECTIVE_HANDLING)?.value
|
|
109
|
+
const wait = Number.parseInt(preferences.get(DIRECTIVE_WAIT)?.value ?? '')
|
|
110
|
+
const timezone = preferences.get(DIRECTIVE_TIMEZONE)?.value
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
asynchronous,
|
|
114
|
+
representation,
|
|
115
|
+
handling,
|
|
116
|
+
wait: Number.isFinite(wait) ? wait : undefined,
|
|
117
|
+
timezone,
|
|
118
|
+
preferences
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class AppliedPreferences {
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Map<string, string|undefined>} preferences
|
|
127
|
+
*/
|
|
128
|
+
static #encode_Map(preferences) {
|
|
129
|
+
return [ ...preferences.entries()
|
|
130
|
+
.map(([ key, value ]) => {
|
|
131
|
+
// todo check if value should be quoted
|
|
132
|
+
if(value !== undefined) { return `${key}${SEPARATOR.KVP}${value}` }
|
|
133
|
+
return key
|
|
134
|
+
}) ]
|
|
135
|
+
.join(SEPARATOR.PREFERENCE)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {Partial<AppliedRequestPreferences>|Map<string, string|undefined>|undefined} preferences
|
|
140
|
+
*/
|
|
141
|
+
static encode(preferences) {
|
|
142
|
+
if(preferences === undefined) { return undefined}
|
|
143
|
+
if(preferences instanceof Map) {
|
|
144
|
+
return AppliedPreferences.#encode_Map(preferences)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const applied = new Map()
|
|
148
|
+
|
|
149
|
+
for(const [ key, pref ] of preferences.preferences?.entries() ?? []) {
|
|
150
|
+
applied.set(key, pref?.value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if(preferences.asynchronous === true) { applied.set(DIRECTIVE_RESPOND_ASYNC, undefined) }
|
|
154
|
+
if(preferences.asynchronous === false) { applied.delete(DIRECTIVE_RESPOND_ASYNC) }
|
|
155
|
+
if(preferences.representation !== undefined) { applied.set(DIRECTIVE_REPRESENTATION, preferences.representation) }
|
|
156
|
+
if(preferences.handling !== undefined) { applied.set(DIRECTIVE_HANDLING, preferences.handling) }
|
|
157
|
+
if(preferences.wait !== undefined) { applied.set(DIRECTIVE_WAIT, preferences.wait) }
|
|
158
|
+
if(preferences.timezone !== undefined) { applied.set(DIRECTIVE_TIMEZONE, preferences.timezone) }
|
|
159
|
+
|
|
160
|
+
return AppliedPreferences.#encode_Map(applied)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
// console.log(AppliedPreferences.encode(undefined))
|
|
166
|
+
// console.log(AppliedPreferences.encode({ }))
|
|
167
|
+
// console.log(AppliedPreferences.encode({ wait: 10 }))
|
|
168
|
+
// console.log(AppliedPreferences.encode({ asynchronous: undefined }))
|
|
169
|
+
// console.log(AppliedPreferences.encode({ asynchronous: false }))
|
|
170
|
+
// console.log(AppliedPreferences.encode({ asynchronous: true }))
|
|
171
|
+
// console.log(AppliedPreferences.encode({ preferences: new Map([
|
|
172
|
+
// [ 'respond-async', { value: undefined } ]
|
|
173
|
+
// ]) }))
|
|
174
|
+
// console.log(AppliedPreferences.encode({
|
|
175
|
+
// asynchronous: false,
|
|
176
|
+
// preferences: new Map([
|
|
177
|
+
// [ 'respond-async', { value: 'fake' } ]
|
|
178
|
+
// ]) }))
|
|
179
|
+
// console.log(AppliedPreferences.encode({ asynchronous: true, wait: 100 }))
|
|
180
|
+
// console.log(AppliedPreferences.encode({
|
|
181
|
+
// representation: DIRECTIVE_REPRESENTATION_MINIMAL,
|
|
182
|
+
// preferences: new Map([
|
|
183
|
+
// ['foo', { value: 'bar', parameters: new Map([ [ 'biz', 'bang' ] ]) } ],
|
|
184
|
+
// [ 'fake', undefined ]
|
|
185
|
+
// ])
|
|
186
|
+
// }))
|
|
187
|
+
|
|
188
|
+
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
189
|
+
// console.log(Preferences.parse(' foo; bar')?.preferences)
|
|
190
|
+
// console.log(Preferences.parse(' foo; bar=""')?.preferences)
|
|
191
|
+
// console.log(Preferences.parse(' foo=""; bar')?.preferences)
|
|
192
|
+
// console.log(Preferences.parse(' foo =""; bar;biz; bang ')?.preferences)
|
|
193
|
+
// console.log(Preferences.parse('return=minimal; foo="some parameter"')?.preferences)
|
|
194
|
+
|
|
195
|
+
// console.log(Preferences.parse('timezone=America/Los_Angeles'))
|
|
196
|
+
// console.log(Preferences.parse('return=headers-only'))
|
|
197
|
+
// console.log(Preferences.parse('return=minimal'))
|
|
198
|
+
// console.log(Preferences.parse('return=representation'))
|
|
199
|
+
// console.log(Preferences.parse('respond-async, wait=10`'))
|
|
200
|
+
// console.log(Preferences.parse('priority=5'))
|
|
201
|
+
// console.log(Preferences.parse('foo; bar'))
|
|
202
|
+
// console.log(Preferences.parse('foo; bar=""'))
|
|
203
|
+
// console.log(Preferences.parse('foo=""; bar'))
|
|
204
|
+
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
205
|
+
// console.log(Preferences.parse('return=minimal; foo="some parameter"'))
|
package/src/range.js
CHANGED
|
@@ -77,7 +77,7 @@ export class Range {
|
|
|
77
77
|
return { start, end: RANGE_EMPTY }
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
if(!Number.isInteger(start)
|
|
80
|
+
if(!(Number.isInteger(start) && Number.isInteger(end))) { return undefined }
|
|
81
81
|
return { start, end }
|
|
82
82
|
})
|
|
83
83
|
.filter(range => range !== undefined)
|
|
@@ -105,19 +105,14 @@ export class Range {
|
|
|
105
105
|
return { start, end }
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
const exceeds = normalizedRanges.reduce((acc, value) =>
|
|
109
|
-
return acc || (value.start >= contentLength) || (value.end >= contentLength)
|
|
110
|
-
}, false)
|
|
108
|
+
const exceeds = normalizedRanges.reduce((acc, value) => (acc || (value.start >= contentLength) || (value.end >= contentLength)), false)
|
|
111
109
|
|
|
112
|
-
const overlap = normalizedRanges
|
|
110
|
+
const { overlap } = normalizedRanges
|
|
113
111
|
.toSorted((a, b) => a.start - b.start)
|
|
114
|
-
.reduce((acc, item) => {
|
|
115
|
-
return {
|
|
112
|
+
.reduce((acc, item) => ({
|
|
116
113
|
overlap: acc.overlap || acc.end > item.start,
|
|
117
114
|
end: item.end
|
|
118
|
-
}
|
|
119
|
-
}, { overlap: false, end: 0 })
|
|
120
|
-
.overlap
|
|
115
|
+
}), { overlap: false, end: 0 })
|
|
121
116
|
|
|
122
117
|
return {
|
|
123
118
|
units: directive.units,
|
package/src/response/accepted.js
CHANGED
package/src/response/bytes.js
CHANGED
package/src/response/conflict.js
CHANGED
package/src/response/created.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { Conditional } from '../conditional.js'
|
|
4
|
+
import { send } from './send-util.js'
|
|
4
5
|
|
|
5
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
7
|
/** @import { Metadata } from './defs.js' */
|
package/src/response/defs.js
CHANGED
|
@@ -10,6 +10,8 @@ export const HTTP_HEADER_SEC_FETCH_MODE = 'sec-fetch-mode'
|
|
|
10
10
|
export const HTTP_HEADER_SEC_FETCH_DEST = 'sec-fetch-dest'
|
|
11
11
|
export const HTTP_HEADER_ACCEPT_POST = 'accept-post'
|
|
12
12
|
export const HTTP_HEADER_ACCEPT_PATCH = 'accept-patch'
|
|
13
|
+
export const HTTP_HEADER_CLEAR_SITE_DATE = 'clear-site-data'
|
|
14
|
+
export const HTTP_HEADER_PREFERENCE_APPLIED = 'preference-applied'
|
|
13
15
|
|
|
14
16
|
export const HTTP_METHOD_QUERY = 'QUERY'
|
|
15
17
|
export const HTTP_HEADER_ACCEPT_QUERY = 'accept-query'
|
package/src/response/error.js
CHANGED
package/src/response/gone.js
CHANGED
package/src/response/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
/** biome-ignore-all lint/performance/noBarrelFile: entry point */
|
|
2
|
+
/** biome-ignore-all lint/performance/noReExportAll: entry point */
|
|
1
3
|
export * from './defs.js'
|
|
2
4
|
export * from './send-util.js'
|
|
5
|
+
// end common headers
|
|
3
6
|
|
|
4
7
|
export * from './accepted.js'
|
|
5
8
|
export * from './bytes.js'
|
package/src/response/json.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
|
-
import { send_encoded } from './send-util.js'
|
|
4
3
|
import { CONTENT_TYPE_JSON } from '../content-type.js'
|
|
4
|
+
import { send_encoded } from './send-util.js'
|
|
5
5
|
|
|
6
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
7
|
/** @import { Metadata } from './defs.js' */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { Conditional } from '../conditional.js'
|
|
4
|
+
import { send } from './send-util.js'
|
|
4
5
|
|
|
5
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
7
|
/** @import { Metadata } from './defs.js' */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
+
|
|
2
3
|
import { CONTENT_TYPE_JSON } from '../content-type.js'
|
|
3
4
|
import { send } from './send-util.js'
|
|
4
5
|
|
|
@@ -14,7 +15,7 @@ const { HTTP_STATUS_NOT_ACCEPTABLE } = http2.constants
|
|
|
14
15
|
*/
|
|
15
16
|
export function sendNotAcceptable(stream, supportedTypes, meta) {
|
|
16
17
|
const supportedTypesList = Array.isArray(supportedTypes) ? supportedTypes : [ supportedTypes ]
|
|
17
|
-
const has = supportedTypesList.length
|
|
18
|
+
const has = supportedTypesList.length > 0
|
|
18
19
|
|
|
19
20
|
send(stream,
|
|
20
21
|
HTTP_STATUS_NOT_ACCEPTABLE,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
import { Conditional } from '../conditional.js'
|
|
2
|
+
|
|
4
3
|
import { CacheControl } from '../cache-control.js'
|
|
4
|
+
import { Conditional } from '../conditional.js'
|
|
5
|
+
import { send } from './send-util.js'
|
|
5
6
|
|
|
6
7
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
8
|
/** @import { Metadata } from './defs.js' */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
|
-
import { send_bytes } from './send-util.js'
|
|
4
|
-
import { RANGE_UNITS_BYTES } from "./defs.js"
|
|
5
|
-
import { Multipart } from '../multipart.js'
|
|
6
3
|
import { MIME_TYPE_MULTIPART_RANGE } from '../content-type.js'
|
|
4
|
+
import { Multipart } from '../multipart.js'
|
|
5
|
+
import { RANGE_UNITS_BYTES } from "./defs.js"
|
|
6
|
+
import { send_bytes } from './send-util.js'
|
|
7
7
|
|
|
8
8
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
9
9
|
/** @import { Metadata } from './defs.js' */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { CONTENT_RANGE_UNKNOWN, ContentRange } from '../content-range.js'
|
|
4
|
+
import { send } from './send-util.js'
|
|
4
5
|
|
|
5
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
7
|
/** @import { Metadata } from './defs.js' */
|
|
@@ -8,16 +8,16 @@ import {
|
|
|
8
8
|
zstdCompressSync
|
|
9
9
|
} from 'node:zlib'
|
|
10
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'
|
|
11
16
|
import {
|
|
12
17
|
coreHeaders,
|
|
13
18
|
customHeaders,
|
|
14
19
|
performanceHeaders
|
|
15
20
|
} from './header-util.js'
|
|
16
|
-
import { ContentRange } from '../content-range.js'
|
|
17
|
-
import { CacheControl } from '../cache-control.js'
|
|
18
|
-
import { Conditional } from '../conditional.js'
|
|
19
|
-
import { HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
|
|
20
|
-
import { CHARSET_UTF8 } from '../content-type.js'
|
|
21
21
|
|
|
22
22
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
23
23
|
/** @import { IncomingHttpHeaders } from 'node:http2' */
|
|
@@ -29,6 +29,12 @@ import { CHARSET_UTF8 } from '../content-type.js'
|
|
|
29
29
|
|
|
30
30
|
/** @typedef {ArrayBufferLike|ArrayBufferView|ReadableStream|string} SendBody */
|
|
31
31
|
|
|
32
|
+
const {
|
|
33
|
+
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
34
|
+
HTTP_STATUS_NOT_FOUND,
|
|
35
|
+
HTTP_STATUS_UNAUTHORIZED
|
|
36
|
+
} = http2.constants
|
|
37
|
+
|
|
32
38
|
const {
|
|
33
39
|
HTTP2_HEADER_CONTENT_ENCODING,
|
|
34
40
|
HTTP2_HEADER_VARY,
|
|
@@ -138,9 +144,9 @@ export function send_bytes(stream, status, contentType, obj, range, contentLengt
|
|
|
138
144
|
*/
|
|
139
145
|
export function send(stream, status, headers, exposedHeaders, contentType, body, meta) {
|
|
140
146
|
// if(status >= 400) { console.warn(status, body) }
|
|
141
|
-
if(status ===
|
|
142
|
-
if(status ===
|
|
143
|
-
if(status >=
|
|
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) }
|
|
144
150
|
// console.log('SEND', status, body?.byteLength)
|
|
145
151
|
|
|
146
152
|
if(stream === undefined) { console.log('send - end stream undef'); return }
|
package/src/response/sse.js
CHANGED
|
@@ -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
|
|
package/src/response/timeout.js
CHANGED
package/src/response/trace.js
CHANGED
|
@@ -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(
|
|
34
|
-
|
|
37
|
+
.join(LINE_ENDING),
|
|
38
|
+
LINE_ENDING
|
|
35
39
|
]
|
|
36
|
-
.join(
|
|
40
|
+
.join(LINE_ENDING)
|
|
37
41
|
|
|
38
42
|
send(stream, HTTP_STATUS_OK, {}, [], CONTENT_TYPE_MESSAGE_HTTP, reconstructed, meta)
|
|
39
43
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
|
|
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
|
/**
|
|
@@ -14,13 +17,16 @@ const { HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE } = http2.constants
|
|
|
14
17
|
* @param {Metadata} meta
|
|
15
18
|
*/
|
|
16
19
|
export function sendUnsupportedMediaType(stream, acceptableMediaType, supportedQueryTypes, meta) {
|
|
17
|
-
const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ]
|
|
18
|
-
|
|
19
20
|
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
20
21
|
const exposedHeaders = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY, HTTP_HEADER_ACCEPT_POST ] : [ HTTP_HEADER_ACCEPT_POST ]
|
|
21
22
|
|
|
23
|
+
const method = HTTP2_METHOD_POST // todo pass in as parameter or split acceptable to post and patch types
|
|
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
|
|
27
|
+
|
|
22
28
|
send(stream, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, {
|
|
23
|
-
[
|
|
29
|
+
[acceptHeader]: acceptValue,
|
|
24
30
|
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
|
|
25
31
|
}, exposedHeaders, undefined, undefined, meta)
|
|
26
32
|
}
|