@johntalton/http-util 6.0.0 → 6.1.0
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 +16 -2
- package/src/defs.js +34 -1
- package/src/headers/accept-encoding.js +9 -12
- package/src/headers/accept-language.js +10 -4
- package/src/headers/accept.js +0 -28
- package/src/headers/cache-control.js +0 -6
- package/src/headers/clear-site-data.js +4 -10
- package/src/headers/client-hints.js +9 -2
- package/src/headers/conditional.js +169 -105
- package/src/headers/content-disposition.js +0 -14
- package/src/headers/content-range.js +0 -17
- package/src/headers/content-type.js +1 -1
- package/src/headers/forwarded.js +2 -33
- package/src/headers/link.js +10 -11
- package/src/headers/multipart.js +17 -9
- package/src/headers/preference.js +0 -43
- package/src/headers/range.js +3 -31
- package/src/headers/rate-limit.js +6 -1
- package/src/headers/server-timing.js +1 -14
- package/src/headers/strict-transport-security.js +1 -0
- package/src/headers/util/mime.js +2 -2
- package/src/headers/util/whitespace.js +3 -1
- package/src/response/2xx/bytes.js +41 -6
- package/src/response/2xx/created.js +26 -5
- package/src/response/2xx/json.js +36 -4
- package/src/response/2xx/no-content.js +25 -5
- package/src/response/2xx/partial-content.js +38 -8
- package/src/response/2xx/preflight.js +26 -7
- package/src/response/3xx/found.js +23 -0
- package/src/response/3xx/not-modified.js +27 -3
- package/src/response/4xx/bad-request.js +19 -0
- package/src/response/4xx/not-acceptable.js +12 -1
- package/src/response/4xx/not-allowed.js +15 -4
- package/src/response/4xx/payment-required.js +17 -0
- package/src/response/4xx/precondition-failed.js +32 -3
- package/src/response/4xx/range-not-satisfiable.js +12 -1
- package/src/response/4xx/too-many-requests.js +18 -1
- package/src/response/4xx/unauthorized.js +1 -1
- package/src/response/4xx/unsupported-media.js +19 -2
- package/src/response/5xx/unavailable.js +13 -1
- package/src/response/index.js +4 -0
- package/src/response/response.js +6 -0
- package/src/response/send-util.js +23 -10
package/src/headers/link.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} LinkItem
|
|
3
|
-
* @property {string} url
|
|
3
|
+
* @property {URL|string} url
|
|
4
4
|
* @property {string|undefined} [relation]
|
|
5
5
|
* @property {Map<string, string>|undefined} [parameters]
|
|
6
6
|
*/
|
|
@@ -10,7 +10,7 @@ export class Link {
|
|
|
10
10
|
* @param {LinkItem} link
|
|
11
11
|
*/
|
|
12
12
|
static *#encode(link) {
|
|
13
|
-
const encodedUri = encodeURI(link.url)
|
|
13
|
+
const encodedUri = (link.url instanceof URL) ? link.url : encodeURI(link.url)
|
|
14
14
|
|
|
15
15
|
yield `<${encodedUri}>`
|
|
16
16
|
if(link.relation !== undefined) { yield `rel="${link.relation}"` }
|
|
@@ -20,16 +20,15 @@ export class Link {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
/**
|
|
25
|
-
* @param {LinkItem}
|
|
24
|
+
* @param {Array<LinkItem>|LinkItem|undefined} links
|
|
26
25
|
*/
|
|
27
|
-
static encode(
|
|
28
|
-
|
|
26
|
+
static encode(links) {
|
|
27
|
+
if(links === undefined) { return undefined }
|
|
28
|
+
const linkAry = Array.isArray(links) ? links : [ links ]
|
|
29
|
+
if(linkAry.length === 0) { return undefined }
|
|
30
|
+
return linkAry
|
|
31
|
+
.map(link => [ ...Link.#encode(link) ].join('; '))
|
|
32
|
+
.join(', ')
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
|
|
32
|
-
// console.log(Link.encode({ url: '/index.html', parameters: new Map([ [ 'as', 'style' ], [ 'fetchpriority', 'high' ] ]) }))
|
|
33
|
-
// console.log(Link.encode({ url: '/index.html', relation: 'next', parameters: new Map([ [ 'fetchpriority', 'high' ] ]) }))
|
|
34
|
-
// console.log(Link.encode({ url: '/index.html', relation: 'next' }))
|
|
35
|
-
// console.log(Link.encode({ url: 'https://example.com/苗条', relation: 'preconnect' }))
|
package/src/headers/multipart.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReadableStream } from 'node:stream/web'
|
|
2
2
|
|
|
3
|
+
import { isQuoted, stripQuotes } from '../headers/util/quote.js'
|
|
3
4
|
import { ContentDisposition } from './content-disposition.js'
|
|
4
5
|
import { ContentRange } from './content-range.js'
|
|
5
6
|
import { ContentType } from './content-type.js'
|
|
@@ -49,11 +50,14 @@ export class Multipart {
|
|
|
49
50
|
* @param {string} text
|
|
50
51
|
* @param {string} boundary
|
|
51
52
|
* @param {string} [_charset='utf8']
|
|
53
|
+
* @returns {FormData}
|
|
52
54
|
*/
|
|
53
55
|
static parse_FormData(text, boundary, _charset = 'utf8') {
|
|
54
|
-
// console.log({ boundary, text })
|
|
55
56
|
const formData = new FormData()
|
|
56
57
|
|
|
58
|
+
if(text === undefined) { return formData }
|
|
59
|
+
if(boundary === undefined) { return formData }
|
|
60
|
+
|
|
57
61
|
if(text === '') {
|
|
58
62
|
// empty body
|
|
59
63
|
return formData
|
|
@@ -94,8 +98,8 @@ export class Multipart {
|
|
|
94
98
|
const name = rawName?.toLowerCase()
|
|
95
99
|
// console.log('header', name, value)
|
|
96
100
|
if(name === MULTIPART_HEADER.CONTENT_TYPE) {
|
|
97
|
-
const
|
|
98
|
-
//
|
|
101
|
+
const contentType = ContentType.parse(value)
|
|
102
|
+
//console.log({ contentType })
|
|
99
103
|
}
|
|
100
104
|
else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
|
|
101
105
|
const disposition = ContentDisposition.parse(value)
|
|
@@ -103,8 +107,7 @@ export class Multipart {
|
|
|
103
107
|
throw new Error('disposition not form-data')
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
partName = disposition.name?.slice(1, -1)
|
|
110
|
+
partName = isQuoted(disposition.name) ? stripQuotes(disposition.name) : disposition.name
|
|
108
111
|
}
|
|
109
112
|
else {
|
|
110
113
|
// unsupported part header - ignore
|
|
@@ -150,6 +153,7 @@ export class Multipart {
|
|
|
150
153
|
|
|
151
154
|
return new ReadableStream({
|
|
152
155
|
type: 'bytes',
|
|
156
|
+
// async pull(controller) {},
|
|
153
157
|
async start(controller) {
|
|
154
158
|
const encoder = new TextEncoder()
|
|
155
159
|
|
|
@@ -158,7 +162,6 @@ export class Multipart {
|
|
|
158
162
|
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_TYPE}: ${contentType}${MULTIPART_SEPARATOR}`))
|
|
159
163
|
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_RANGE}: ${ContentRange.encode({ ...part.range, size: contentLength })}${MULTIPART_SEPARATOR}`))
|
|
160
164
|
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
161
|
-
// controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
162
165
|
|
|
163
166
|
if(part.obj instanceof ReadableStream) {
|
|
164
167
|
// biome-ignore lint/performance/noAwaitInLoops: readable
|
|
@@ -175,7 +178,10 @@ export class Multipart {
|
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
|
-
else if(part.obj instanceof ArrayBuffer
|
|
181
|
+
else if(part.obj instanceof ArrayBuffer) {
|
|
182
|
+
controller.enqueue(new Uint8Array(part.obj))
|
|
183
|
+
}
|
|
184
|
+
else if(ArrayBuffer.isView(part.obj)) {
|
|
179
185
|
controller.enqueue(part.obj)
|
|
180
186
|
}
|
|
181
187
|
else if(typeof part.obj === 'string'){
|
|
@@ -183,7 +189,7 @@ export class Multipart {
|
|
|
183
189
|
}
|
|
184
190
|
else {
|
|
185
191
|
// console.log('error', typeof part.obj, part.obj)
|
|
186
|
-
|
|
192
|
+
controller.error(new Error('unknown part type'))
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
@@ -191,7 +197,9 @@ export class Multipart {
|
|
|
191
197
|
|
|
192
198
|
controller.enqueue(encoder.encode(boundaryEnd))
|
|
193
199
|
|
|
194
|
-
|
|
200
|
+
// controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
201
|
+
|
|
202
|
+
controller.close()
|
|
195
203
|
}
|
|
196
204
|
})
|
|
197
205
|
}
|
|
@@ -128,46 +128,3 @@ export class AppliedPreferences {
|
|
|
128
128
|
return AppliedPreferences.#encode_Map(applied)
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// console.log(AppliedPreferences.encode(undefined))
|
|
134
|
-
// console.log(AppliedPreferences.encode({ }))
|
|
135
|
-
// console.log(AppliedPreferences.encode({ wait: 10 }))
|
|
136
|
-
// console.log(AppliedPreferences.encode({ asynchronous: undefined }))
|
|
137
|
-
// console.log(AppliedPreferences.encode({ asynchronous: false }))
|
|
138
|
-
// console.log(AppliedPreferences.encode({ asynchronous: true }))
|
|
139
|
-
// console.log(AppliedPreferences.encode({ preferences: new Map([
|
|
140
|
-
// [ 'respond-async', { value: undefined } ]
|
|
141
|
-
// ]) }))
|
|
142
|
-
// console.log(AppliedPreferences.encode({
|
|
143
|
-
// asynchronous: false,
|
|
144
|
-
// preferences: new Map([
|
|
145
|
-
// [ 'respond-async', { value: 'fake' } ]
|
|
146
|
-
// ]) }))
|
|
147
|
-
// console.log(AppliedPreferences.encode({ asynchronous: true, wait: 100 }))
|
|
148
|
-
// console.log(AppliedPreferences.encode({
|
|
149
|
-
// representation: DIRECTIVE_REPRESENTATION_MINIMAL,
|
|
150
|
-
// preferences: new Map([
|
|
151
|
-
// ['foo', { value: 'bar', parameters: new Map([ [ 'biz', 'bang' ] ]) } ],
|
|
152
|
-
// [ 'fake', undefined ]
|
|
153
|
-
// ])
|
|
154
|
-
// }))
|
|
155
|
-
|
|
156
|
-
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
157
|
-
// console.log(Preferences.parse(' foo; bar')?.preferences)
|
|
158
|
-
// console.log(Preferences.parse(' foo; bar=""')?.preferences)
|
|
159
|
-
// console.log(Preferences.parse(' foo=""; bar')?.preferences)
|
|
160
|
-
// console.log(Preferences.parse(' foo =""; bar;biz; bang ')?.preferences)
|
|
161
|
-
// console.log(Preferences.parse('return=minimal; foo="some parameter"')?.preferences)
|
|
162
|
-
|
|
163
|
-
// console.log(Preferences.parse('timezone=America/Los_Angeles'))
|
|
164
|
-
// console.log(Preferences.parse('return=headers-only'))
|
|
165
|
-
// console.log(Preferences.parse('return=minimal'))
|
|
166
|
-
// console.log(Preferences.parse('return=representation'))
|
|
167
|
-
// console.log(Preferences.parse('respond-async, wait=10`'))
|
|
168
|
-
// console.log(Preferences.parse('priority=5'))
|
|
169
|
-
// console.log(Preferences.parse('foo; bar'))
|
|
170
|
-
// console.log(Preferences.parse('foo; bar=""'))
|
|
171
|
-
// console.log(Preferences.parse('foo=""; bar'))
|
|
172
|
-
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
173
|
-
// console.log(Preferences.parse('return=minimal; foo="some parameter"'))
|
package/src/headers/range.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { RANGE_UNITS_BYTES } from '../defs.js'
|
|
2
2
|
|
|
3
|
+
/** @import { AcceptRangeUnits } from '../defs.js' */
|
|
4
|
+
|
|
3
5
|
export const RANGE_EQUAL = '='
|
|
4
6
|
export const RANGE_SEPARATOR = '-'
|
|
5
7
|
export const RANGE_LIST_SEPARATOR = ','
|
|
@@ -32,7 +34,7 @@ export const RANGE_EMPTY = ''
|
|
|
32
34
|
/**
|
|
33
35
|
* @template RV
|
|
34
36
|
* @typedef {Object} RangeDirective
|
|
35
|
-
* @property {
|
|
37
|
+
* @property {AcceptRangeUnits|undefined} units
|
|
36
38
|
* @property {Array<RV>} ranges
|
|
37
39
|
*/
|
|
38
40
|
|
|
@@ -122,33 +124,3 @@ export class Range {
|
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
|
-
|
|
126
|
-
// console.log(Range.parse(''))
|
|
127
|
-
// console.log(Range.parse('='))
|
|
128
|
-
// console.log(Range.parse('foo'))
|
|
129
|
-
// console.log(Range.parse('bytes'))
|
|
130
|
-
// console.log(Range.parse('bytes='))
|
|
131
|
-
// console.log(Range.parse('bytes=-'))
|
|
132
|
-
// console.log(Range.parse('bytes=foo'))
|
|
133
|
-
// console.log(Range.parse('bytes=0-foo'))
|
|
134
|
-
// console.log(Range.parse('bytes=0-0xff'))
|
|
135
|
-
// console.log()
|
|
136
|
-
// console.log(Range.parse('bytes=1024-'))
|
|
137
|
-
// console.log(Range.parse('bytes=-1024'))
|
|
138
|
-
// console.log(Range.parse('bytes=0-1024'))
|
|
139
|
-
// console.log()
|
|
140
|
-
// console.log(Range.parse('bytes=0-0,-1'))
|
|
141
|
-
// console.log(Range.parse('bytes=0-1024, -1024'))
|
|
142
|
-
// console.log(Range.parse('bytes= 0-999, 4500-5499, -1000'))
|
|
143
|
-
// console.log(Range.parse('bytes=500-600,601-999'))
|
|
144
|
-
|
|
145
|
-
// console.log('------')
|
|
146
|
-
// console.log(Range.normalize(Range.parse('bytes=1024-'), 5000))
|
|
147
|
-
// console.log(Range.normalize(Range.parse('bytes=-1024'), 5000))
|
|
148
|
-
// console.log(Range.normalize(Range.parse('bytes=0-1024'), 5000))
|
|
149
|
-
// console.log(Range.normalize(Range.parse('bytes=0-0,-1'), 10000)) // 0 and 9999
|
|
150
|
-
// console.log(Range.normalize(Range.parse('bytes=0-1024, -1024'), 5000))
|
|
151
|
-
// console.log(Range.normalize(Range.parse('bytes= 0-999, 4500-5499, -1000'), 5000))
|
|
152
|
-
// console.log(Range.normalize(Range.parse('bytes=500-600,601-999'), 5000))
|
|
153
|
-
|
|
154
|
-
// console.log(Range.normalize(Range.parse('bytes=-500'), 10000)) // 9500-9999
|
|
@@ -45,6 +45,7 @@ export class RateLimit {
|
|
|
45
45
|
* @param {RateLimitInfo} limitInfo
|
|
46
46
|
*/
|
|
47
47
|
static from(limitInfo) {
|
|
48
|
+
if(limitInfo === undefined) { return undefined }
|
|
48
49
|
const { name, remaining, resetSeconds, partitionKey } = limitInfo
|
|
49
50
|
|
|
50
51
|
if(name === undefined || remaining === undefined) { return undefined }
|
|
@@ -61,8 +62,12 @@ export class RateLimitPolicy {
|
|
|
61
62
|
*/
|
|
62
63
|
static from(...policies) {
|
|
63
64
|
if(policies === undefined) { return undefined }
|
|
65
|
+
if(policies.length === 0) { return undefined }
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const remainingPolicies = policies.filter(pol => pol !== undefined)
|
|
68
|
+
if(remainingPolicies.length === 0) { return undefined }
|
|
69
|
+
|
|
70
|
+
return remainingPolicies
|
|
66
71
|
.filter(policy => policy.name !== undefined && policy.quota !== undefined)
|
|
67
72
|
.map(policy => {
|
|
68
73
|
const {
|
|
@@ -19,7 +19,7 @@ export const SERVER_TIMING_SEPARATOR = {
|
|
|
19
19
|
|
|
20
20
|
export class ServerTiming {
|
|
21
21
|
/**
|
|
22
|
-
* @param {Array<TimingsInfo
|
|
22
|
+
* @param {Array<TimingsInfo>|undefined} timings
|
|
23
23
|
*/
|
|
24
24
|
static encode(timings) {
|
|
25
25
|
if(timings === undefined) { return undefined }
|
|
@@ -36,16 +36,3 @@ export class ServerTiming {
|
|
|
36
36
|
.join(SERVER_TIMING_SEPARATOR.METRIC)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// console.log(ServerTiming.encode([{ name: 'missedCache' }]))
|
|
42
|
-
// console.log(ServerTiming.encode([{ name: 'cpu', duration: 2.4 }]))
|
|
43
|
-
|
|
44
|
-
// // cache;desc="Cache Read";dur=23.2
|
|
45
|
-
// console.log(ServerTiming.encode([{ name: 'cache', duration: 23.2, description: "Cache Read" }]))
|
|
46
|
-
|
|
47
|
-
// // db;dur=53, app;dur=47.2
|
|
48
|
-
// console.log(ServerTiming.encode([
|
|
49
|
-
// { name: 'db', duration: 54 },
|
|
50
|
-
// { name: 'app', duration: 47.2 }
|
|
51
|
-
// ]))
|
package/src/headers/util/mime.js
CHANGED
|
@@ -66,10 +66,10 @@ export class Mime {
|
|
|
66
66
|
// if(candidateSubtype === '') { return undefined }
|
|
67
67
|
if(hasSpecialChar(candidateSubtype)) { return undefined }
|
|
68
68
|
|
|
69
|
-
const subtype = (candidateSubtype === '') ? MIME_ANY : candidateSubtype ?? MIME_ANY
|
|
69
|
+
const subtype = (candidateSubtype === '') ? MIME_ANY : (candidateSubtype ?? MIME_ANY)
|
|
70
70
|
|
|
71
71
|
return {
|
|
72
|
-
mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype
|
|
72
|
+
mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype}`,
|
|
73
73
|
type,
|
|
74
74
|
subtype
|
|
75
75
|
}
|
|
@@ -3,10 +3,9 @@ import http2 from 'node:http2'
|
|
|
3
3
|
import { send_bytes } from '../send-util.js'
|
|
4
4
|
|
|
5
5
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
-
/** @import { Metadata } from '../../defs.js' */
|
|
7
|
-
/** @import { EtagItem } from '../../headers/conditional.js' */
|
|
6
|
+
/** @import { AcceptRangeUnits, SendContent, SendInfo, Metadata, SendBody } from '../../defs.js' */
|
|
7
|
+
/** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
|
|
8
8
|
/** @import { CacheControlOptions } from '../../headers/cache-control.js' */
|
|
9
|
-
/** @import { SendBody } from '../send-util.js' */
|
|
10
9
|
|
|
11
10
|
const { HTTP_STATUS_OK } = http2.constants
|
|
12
11
|
|
|
@@ -17,11 +16,47 @@ const { HTTP_STATUS_OK } = http2.constants
|
|
|
17
16
|
* @param {number|undefined} contentLength
|
|
18
17
|
* @param {string|undefined} encoding
|
|
19
18
|
* @param {EtagItem|undefined} etag
|
|
19
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
20
20
|
* @param {number|undefined} age
|
|
21
21
|
* @param {CacheControlOptions} cacheControl
|
|
22
|
-
* @param {
|
|
22
|
+
* @param {AcceptRangeUnits|undefined} acceptRanges
|
|
23
23
|
* @param {Metadata} meta
|
|
24
24
|
*/
|
|
25
|
-
export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, age, cacheControl, acceptRanges, meta) {
|
|
26
|
-
|
|
25
|
+
export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, meta) {
|
|
26
|
+
_sendBytes(stream, obj, {
|
|
27
|
+
contentType,
|
|
28
|
+
contentLength,
|
|
29
|
+
encoding,
|
|
30
|
+
etag,
|
|
31
|
+
lastModified,
|
|
32
|
+
age,
|
|
33
|
+
cacheControl
|
|
34
|
+
}, {
|
|
35
|
+
acceptRanges
|
|
36
|
+
}, meta)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {ServerHttp2Stream} stream
|
|
41
|
+
* @param {SendBody|undefined} obj
|
|
42
|
+
* @param {Omit<SendContent, 'rangeDirective'>} content
|
|
43
|
+
* @param {Pick<SendInfo, 'acceptRanges'>} info
|
|
44
|
+
* @param {Metadata} meta
|
|
45
|
+
*/
|
|
46
|
+
export function _sendBytes(stream, obj, content, info, meta) {
|
|
47
|
+
const {
|
|
48
|
+
contentType,
|
|
49
|
+
contentLength,
|
|
50
|
+
encoding,
|
|
51
|
+
etag,
|
|
52
|
+
lastModified,
|
|
53
|
+
age,
|
|
54
|
+
cacheControl,
|
|
55
|
+
} = content
|
|
56
|
+
|
|
57
|
+
const { acceptRanges } = info
|
|
58
|
+
|
|
59
|
+
const supportedQueryType = undefined
|
|
60
|
+
const range = undefined
|
|
61
|
+
send_bytes(stream, HTTP_STATUS_OK, contentType, obj, range, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryType, meta)
|
|
27
62
|
}
|
|
@@ -4,12 +4,13 @@ import { Conditional } from '../../headers/conditional.js'
|
|
|
4
4
|
import { send } from '../send-util.js'
|
|
5
5
|
|
|
6
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
|
-
/** @import { Metadata } from '../../defs.js' */
|
|
8
|
-
/** @import { EtagItem } from '../../headers/conditional.js' */
|
|
7
|
+
/** @import { SendContent, Metadata } from '../../defs.js' */
|
|
8
|
+
/** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
|
|
9
9
|
|
|
10
10
|
const {
|
|
11
11
|
HTTP2_HEADER_LOCATION,
|
|
12
|
-
HTTP2_HEADER_ETAG
|
|
12
|
+
HTTP2_HEADER_ETAG,
|
|
13
|
+
HTTP2_HEADER_LAST_MODIFIED
|
|
13
14
|
} = http2.constants
|
|
14
15
|
|
|
15
16
|
const { HTTP_STATUS_CREATED } = http2.constants
|
|
@@ -18,11 +19,31 @@ const { HTTP_STATUS_CREATED } = http2.constants
|
|
|
18
19
|
* @param {ServerHttp2Stream} stream
|
|
19
20
|
* @param {URL} location
|
|
20
21
|
* @param {EtagItem|undefined} etag
|
|
22
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
21
23
|
* @param {Metadata} meta
|
|
22
24
|
*/
|
|
23
|
-
export function sendCreated(stream, location, etag, meta) {
|
|
25
|
+
export function sendCreated(stream, location, etag, lastModified, meta) {
|
|
26
|
+
_sendCreated(stream, location, {
|
|
27
|
+
etag,
|
|
28
|
+
lastModified
|
|
29
|
+
}, meta)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {ServerHttp2Stream} stream
|
|
34
|
+
* @param {URL} location
|
|
35
|
+
* @param {Pick<SendContent, 'etag' | 'lastModified'>} content
|
|
36
|
+
* @param {Metadata} meta
|
|
37
|
+
*/
|
|
38
|
+
export function _sendCreated(stream, location, content, meta) {
|
|
39
|
+
const {
|
|
40
|
+
etag,
|
|
41
|
+
lastModified
|
|
42
|
+
} = content
|
|
43
|
+
|
|
24
44
|
send(stream, HTTP_STATUS_CREATED, {
|
|
25
45
|
[HTTP2_HEADER_LOCATION]: location.href,
|
|
26
|
-
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
46
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
47
|
+
[HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified)
|
|
27
48
|
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
28
49
|
}
|
package/src/response/2xx/json.js
CHANGED
|
@@ -4,8 +4,8 @@ import { CONTENT_TYPE_JSON } from '../../headers/content-type.js'
|
|
|
4
4
|
import { send_encoded } from '../send-util.js'
|
|
5
5
|
|
|
6
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
|
-
/** @import { Metadata } from '../../defs.js' */
|
|
8
|
-
/** @import { EtagItem } from '../../headers/conditional.js' */
|
|
7
|
+
/** @import { SendContent, SendInfo, Metadata } from '../../defs.js' */
|
|
8
|
+
/** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
|
|
9
9
|
/** @import { CacheControlOptions } from '../../headers/cache-control.js' */
|
|
10
10
|
|
|
11
11
|
const { HTTP_STATUS_OK } = http2.constants
|
|
@@ -15,14 +15,46 @@ const { HTTP_STATUS_OK } = http2.constants
|
|
|
15
15
|
* @param {Object} obj
|
|
16
16
|
* @param {string|undefined} encoding
|
|
17
17
|
* @param {EtagItem|undefined} etag
|
|
18
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
18
19
|
* @param {number|undefined} age
|
|
19
20
|
* @param {CacheControlOptions} cacheControl
|
|
20
21
|
* @param {Array<string>|undefined} supportedQueryTypes
|
|
21
22
|
* @param {Metadata} meta
|
|
22
23
|
*/
|
|
23
|
-
export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, supportedQueryTypes, meta) {
|
|
24
|
+
export function sendJSON_Encoded(stream, obj, encoding, etag, lastModified, age, cacheControl, supportedQueryTypes, meta) {
|
|
25
|
+
_sendJSON_Encoded(stream, obj, {
|
|
26
|
+
encoding,
|
|
27
|
+
etag,
|
|
28
|
+
lastModified,
|
|
29
|
+
age,
|
|
30
|
+
cacheControl
|
|
31
|
+
}, {
|
|
32
|
+
supportedQueryTypes
|
|
33
|
+
}, meta)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {ServerHttp2Stream} stream
|
|
38
|
+
* @param {Object} obj
|
|
39
|
+
* @param {Omit<SendContent, 'contentType' | 'contentLength' | 'rangeDirective'>} content
|
|
40
|
+
* @param {Pick<SendInfo, 'supportedQueryTypes'>} info
|
|
41
|
+
* @param {Metadata} meta
|
|
42
|
+
*/
|
|
43
|
+
export function _sendJSON_Encoded(stream, obj, content, info, meta) {
|
|
44
|
+
const {
|
|
45
|
+
encoding,
|
|
46
|
+
etag,
|
|
47
|
+
lastModified,
|
|
48
|
+
age,
|
|
49
|
+
cacheControl
|
|
50
|
+
} = content
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
supportedQueryTypes
|
|
54
|
+
} = info
|
|
55
|
+
|
|
24
56
|
if(stream.closed) { return }
|
|
25
57
|
|
|
26
58
|
const json = JSON.stringify(obj)
|
|
27
|
-
send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, age, cacheControl, undefined, supportedQueryTypes, meta)
|
|
59
|
+
send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, lastModified, age, cacheControl, undefined, supportedQueryTypes, meta)
|
|
28
60
|
}
|
|
@@ -4,11 +4,12 @@ import { Conditional } from '../../headers/conditional.js'
|
|
|
4
4
|
import { send } from '../send-util.js'
|
|
5
5
|
|
|
6
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
|
-
/** @import { Metadata } from '../../defs.js' */
|
|
8
|
-
/** @import { EtagItem } from '../../headers/conditional.js' */
|
|
7
|
+
/** @import { SendContent, Metadata } from '../../defs.js' */
|
|
8
|
+
/** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
|
|
9
9
|
|
|
10
10
|
const {
|
|
11
|
-
HTTP2_HEADER_ETAG
|
|
11
|
+
HTTP2_HEADER_ETAG,
|
|
12
|
+
HTTP2_HEADER_LAST_MODIFIED
|
|
12
13
|
} = http2.constants
|
|
13
14
|
|
|
14
15
|
const { HTTP_STATUS_NO_CONTENT } = http2.constants
|
|
@@ -16,10 +17,29 @@ const { HTTP_STATUS_NO_CONTENT } = http2.constants
|
|
|
16
17
|
/**
|
|
17
18
|
* @param {ServerHttp2Stream} stream
|
|
18
19
|
* @param {EtagItem|undefined} etag
|
|
20
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
19
21
|
* @param {Metadata} meta
|
|
20
22
|
*/
|
|
21
|
-
export function sendNoContent(stream, etag, meta) {
|
|
23
|
+
export function sendNoContent(stream, etag, lastModified, meta) {
|
|
24
|
+
_sendNoContent(stream, {
|
|
25
|
+
etag,
|
|
26
|
+
lastModified
|
|
27
|
+
}, meta)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {ServerHttp2Stream} stream
|
|
32
|
+
* @param {Pick<SendContent, 'etag' | 'lastModified'>} content
|
|
33
|
+
* @param {Metadata} meta
|
|
34
|
+
*/
|
|
35
|
+
export function _sendNoContent(stream, content, meta) {
|
|
36
|
+
const {
|
|
37
|
+
etag,
|
|
38
|
+
lastModified
|
|
39
|
+
} = content
|
|
40
|
+
|
|
22
41
|
send(stream, HTTP_STATUS_NO_CONTENT, {
|
|
23
|
-
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
42
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
43
|
+
[HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified)
|
|
24
44
|
}, [], undefined, undefined, meta)
|
|
25
45
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
3
|
import { RANGE_UNITS_BYTES } from '../../defs.js'
|
|
4
|
-
import { MIME_TYPE_MULTIPART_RANGE } from '../../headers/content-type.js'
|
|
4
|
+
import { MIME_TYPE_MULTIPART_RANGE, MIME_TYPE_OCTET_STREAM } from '../../headers/content-type.js'
|
|
5
5
|
import { Multipart } from '../../headers/multipart.js'
|
|
6
6
|
import { send_bytes } from '../send-util.js'
|
|
7
7
|
|
|
8
8
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
9
|
-
/** @import { Metadata } from '../../defs.js' */
|
|
10
|
-
/** @import { EtagItem } from '../../headers/conditional.js' */
|
|
9
|
+
/** @import { SendContent, Metadata, SendBody } from '../../defs.js' */
|
|
10
|
+
/** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
|
|
11
11
|
/** @import { CacheControlOptions } from '../../headers/cache-control.js' */
|
|
12
12
|
/** @import { ContentRangeDirective } from '../../headers/content-range.js' */
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
|
|
15
15
|
const { HTTP_STATUS_PARTIAL_CONTENT } = http2.constants
|
|
16
16
|
|
|
@@ -27,23 +27,52 @@ const { HTTP_STATUS_PARTIAL_CONTENT } = http2.constants
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* @param {ServerHttp2Stream} stream
|
|
30
|
-
* @param {string} contentType
|
|
30
|
+
* @param {string|undefined} contentType
|
|
31
31
|
* @param {NonEmptyArray<PartialBytes>|PartialBytes} objs
|
|
32
32
|
* @param {number|undefined} contentLength
|
|
33
33
|
* @param {string|undefined} encoding
|
|
34
34
|
* @param {EtagItem|undefined} etag
|
|
35
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
35
36
|
* @param {number|undefined} age
|
|
36
37
|
* @param {CacheControlOptions} cacheControl
|
|
37
38
|
* @param {Metadata} meta
|
|
38
39
|
*/
|
|
39
|
-
export function sendPartialContent(stream, contentType, objs, contentLength, encoding, etag, age, cacheControl, meta) {
|
|
40
|
+
export function sendPartialContent(stream, contentType, objs, contentLength, encoding, etag, lastModified, age, cacheControl, meta) {
|
|
41
|
+
return _sendPartialContent(stream, objs, {
|
|
42
|
+
contentType,
|
|
43
|
+
contentLength,
|
|
44
|
+
encoding,
|
|
45
|
+
etag,
|
|
46
|
+
lastModified,
|
|
47
|
+
age,
|
|
48
|
+
cacheControl
|
|
49
|
+
}, meta)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {ServerHttp2Stream} stream
|
|
54
|
+
* @param {NonEmptyArray<PartialBytes>|PartialBytes} objs
|
|
55
|
+
* @param {Omit<SendContent, 'rangeDirective'>} content
|
|
56
|
+
* @param {Metadata} meta
|
|
57
|
+
*/
|
|
58
|
+
export function _sendPartialContent(stream, objs, content, meta) {
|
|
59
|
+
const {
|
|
60
|
+
contentType,
|
|
61
|
+
contentLength,
|
|
62
|
+
encoding,
|
|
63
|
+
etag,
|
|
64
|
+
lastModified,
|
|
65
|
+
age,
|
|
66
|
+
cacheControl
|
|
67
|
+
} = content
|
|
68
|
+
|
|
40
69
|
const acceptRanges = RANGE_UNITS_BYTES
|
|
41
70
|
const supportedQueryTypes = undefined
|
|
42
71
|
|
|
43
72
|
if(Array.isArray(objs) && objs.length > 1) {
|
|
44
73
|
// send using multipart bytes
|
|
45
74
|
const boundary = 'PARTIAL_CONTENT_BOUNDARY' // todo make unique for content
|
|
46
|
-
const obj = Multipart.encode_Bytes(contentType, objs, contentLength, boundary)
|
|
75
|
+
const obj = Multipart.encode_Bytes(contentType ?? MIME_TYPE_OCTET_STREAM, objs, contentLength, boundary)
|
|
47
76
|
|
|
48
77
|
const multipartContentType = `${MIME_TYPE_MULTIPART_RANGE}; boundary=${boundary}`
|
|
49
78
|
|
|
@@ -56,6 +85,7 @@ export function sendPartialContent(stream, contentType, objs, contentLength, enc
|
|
|
56
85
|
undefined,
|
|
57
86
|
encoding,
|
|
58
87
|
etag,
|
|
88
|
+
lastModified,
|
|
59
89
|
age,
|
|
60
90
|
cacheControl,
|
|
61
91
|
acceptRanges,
|
|
@@ -67,5 +97,5 @@ export function sendPartialContent(stream, contentType, objs, contentLength, enc
|
|
|
67
97
|
|
|
68
98
|
// single range, send as regular object
|
|
69
99
|
const obj = Array.isArray(objs) ? objs[0] : objs
|
|
70
|
-
send_bytes(stream, HTTP_STATUS_PARTIAL_CONTENT, contentType, obj.obj, obj.range, undefined, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta)
|
|
100
|
+
send_bytes(stream, HTTP_STATUS_PARTIAL_CONTENT, contentType, obj.obj, obj.range, undefined, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta)
|
|
71
101
|
}
|