@johntalton/http-util 4.1.1 → 5.0.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/README.md +232 -21
- package/package.json +1 -1
- package/src/body.js +7 -10
- package/src/cache-control.js +6 -1
- package/src/conditional.js +26 -34
- package/src/content-range.js +59 -0
- package/src/content-type.js +2 -0
- package/src/index.js +3 -0
- package/src/multipart.js +79 -31
- package/src/range.js +159 -0
- package/src/response/bytes.js +26 -0
- package/src/response/content-too-large.js +15 -0
- package/src/response/defs.js +13 -1
- package/src/response/forbidden.js +17 -0
- package/src/response/gone.js +15 -0
- package/src/response/im-a-teapot.js +15 -0
- package/src/response/index.js +14 -0
- package/src/response/insufficient-storage.js +15 -0
- package/src/response/json.js +6 -53
- package/src/response/moved-permanently.js +22 -0
- package/src/response/multiple-choices.js +20 -0
- package/src/response/partial-content.js +71 -0
- package/src/response/permanent-redirect.js +23 -0
- package/src/response/preflight.js +20 -5
- package/src/response/range-not-satisfiable.js +27 -0
- package/src/response/response.js +26 -0
- package/src/response/see-other.js +22 -0
- package/src/response/send-util.js +128 -3
- package/src/response/temporary-redirect.js +22 -0
- package/src/response/unsupported-media.js +9 -4
package/src/range.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { RANGE_UNITS_BYTES } from "./response/defs.js"
|
|
2
|
+
|
|
3
|
+
export const RANGE_EQUAL = '='
|
|
4
|
+
export const RANGE_SEPARATOR = '-'
|
|
5
|
+
export const RANGE_LIST_SEPARATOR = ','
|
|
6
|
+
|
|
7
|
+
/** @type {''} */
|
|
8
|
+
export const RANGE_EMPTY = ''
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} RangeValueFixed
|
|
12
|
+
* @property {number} start
|
|
13
|
+
* @property {number} end
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} RangeValueOpenEnded
|
|
18
|
+
* @property {number} start
|
|
19
|
+
* @property {''} end
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} RangeValueFromEnd
|
|
24
|
+
* @property {''} start
|
|
25
|
+
* @property {number} end
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** @typedef {RangeValueFixed | RangeValueOpenEnded | RangeValueFromEnd} RangeValue */
|
|
29
|
+
|
|
30
|
+
/** @typedef {RangeValueFixed} NormalizedRangeValue */
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template RV
|
|
34
|
+
* @typedef {Object} RangeDirective
|
|
35
|
+
* @property {'bytes'|'none'|undefined} units
|
|
36
|
+
* @property {Array<RV>} ranges
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} RangeDirectiveInfo
|
|
41
|
+
* @property {boolean} exceeds
|
|
42
|
+
* @property {boolean} overlap
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export class Range {
|
|
47
|
+
/**
|
|
48
|
+
* @param {string|undefined} rangeHeader
|
|
49
|
+
* @returns {RangeDirective<RangeValue>|undefined}
|
|
50
|
+
*/
|
|
51
|
+
static parse(rangeHeader) {
|
|
52
|
+
if(rangeHeader === undefined) { return undefined }
|
|
53
|
+
if(!rangeHeader.startsWith(RANGE_UNITS_BYTES)) { return undefined }
|
|
54
|
+
if(!(rangeHeader.substring(RANGE_UNITS_BYTES.length, RANGE_UNITS_BYTES.length + 1) === RANGE_EQUAL)) { return undefined }
|
|
55
|
+
const rangeStr = rangeHeader.substring(RANGE_UNITS_BYTES.length + RANGE_EQUAL.length).trim()
|
|
56
|
+
if(rangeStr === '') { return undefined }
|
|
57
|
+
|
|
58
|
+
const ranges = rangeStr.split(RANGE_LIST_SEPARATOR)
|
|
59
|
+
.map(range => range.trim())
|
|
60
|
+
.map(range => {
|
|
61
|
+
const [ startStr, endStr ] = range.split(RANGE_SEPARATOR)
|
|
62
|
+
if(startStr === undefined) { return undefined }
|
|
63
|
+
if(endStr === undefined) { return undefined }
|
|
64
|
+
if(startStr === RANGE_EMPTY && endStr === RANGE_EMPTY) { return undefined }
|
|
65
|
+
|
|
66
|
+
const start = Number.parseInt(startStr, 10)
|
|
67
|
+
const end = Number.parseInt(endStr, 10)
|
|
68
|
+
|
|
69
|
+
if(startStr === RANGE_EMPTY) {
|
|
70
|
+
if(!Number.isInteger(end)) { return undefined }
|
|
71
|
+
if(end === 0) { return undefined }
|
|
72
|
+
return { start: RANGE_EMPTY, end }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if(endStr === RANGE_EMPTY) {
|
|
76
|
+
if(!Number.isInteger(start)) { return undefined }
|
|
77
|
+
return { start, end: RANGE_EMPTY }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if(!Number.isInteger(start) || !Number.isInteger(end)) { return undefined }
|
|
81
|
+
return { start, end }
|
|
82
|
+
})
|
|
83
|
+
.filter(range => range !== undefined)
|
|
84
|
+
|
|
85
|
+
if(ranges.length === 0) { return undefined }
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
units: RANGE_UNITS_BYTES,
|
|
89
|
+
ranges
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {RangeDirective<RangeValue>|undefined} directive
|
|
95
|
+
* @param {number} contentLength
|
|
96
|
+
* @returns {RangeDirectiveInfo & RangeDirective<NormalizedRangeValue>|undefined}
|
|
97
|
+
*/
|
|
98
|
+
static normalize(directive, contentLength) {
|
|
99
|
+
if(directive === undefined) { return undefined }
|
|
100
|
+
|
|
101
|
+
/** @type {Array<NormalizedRangeValue>} */
|
|
102
|
+
const normalizedRanges = directive.ranges.map(({ start, end }) => {
|
|
103
|
+
if(end === RANGE_EMPTY) { return { start, end: contentLength - 1 } }
|
|
104
|
+
if(start === RANGE_EMPTY) { return { start: contentLength - end, end: contentLength - 1 } }
|
|
105
|
+
return { start, end }
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const exceeds = normalizedRanges.reduce((acc, value) => {
|
|
109
|
+
return acc || (value.start >= contentLength) || (value.end >= contentLength)
|
|
110
|
+
}, false)
|
|
111
|
+
|
|
112
|
+
const overlap = normalizedRanges
|
|
113
|
+
.toSorted((a, b) => a.start - b.start)
|
|
114
|
+
.reduce((acc, item) => {
|
|
115
|
+
return {
|
|
116
|
+
overlap: acc.overlap || acc.end > item.start,
|
|
117
|
+
end: item.end
|
|
118
|
+
}
|
|
119
|
+
}, { overlap: false, end: 0 })
|
|
120
|
+
.overlap
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
units: directive.units,
|
|
124
|
+
overlap,
|
|
125
|
+
exceeds,
|
|
126
|
+
ranges: normalizedRanges
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// console.log(Range.parse(''))
|
|
132
|
+
// console.log(Range.parse('='))
|
|
133
|
+
// console.log(Range.parse('foo'))
|
|
134
|
+
// console.log(Range.parse('bytes'))
|
|
135
|
+
// console.log(Range.parse('bytes='))
|
|
136
|
+
// console.log(Range.parse('bytes=-'))
|
|
137
|
+
// console.log(Range.parse('bytes=foo'))
|
|
138
|
+
// console.log(Range.parse('bytes=0-foo'))
|
|
139
|
+
// console.log(Range.parse('bytes=0-0xff'))
|
|
140
|
+
// console.log()
|
|
141
|
+
// console.log(Range.parse('bytes=1024-'))
|
|
142
|
+
// console.log(Range.parse('bytes=-1024'))
|
|
143
|
+
// console.log(Range.parse('bytes=0-1024'))
|
|
144
|
+
// console.log()
|
|
145
|
+
// console.log(Range.parse('bytes=0-0,-1'))
|
|
146
|
+
// console.log(Range.parse('bytes=0-1024, -1024'))
|
|
147
|
+
// console.log(Range.parse('bytes= 0-999, 4500-5499, -1000'))
|
|
148
|
+
// console.log(Range.parse('bytes=500-600,601-999'))
|
|
149
|
+
|
|
150
|
+
// console.log('------')
|
|
151
|
+
// console.log(Range.normalize(Range.parse('bytes=1024-'), 5000))
|
|
152
|
+
// console.log(Range.normalize(Range.parse('bytes=-1024'), 5000))
|
|
153
|
+
// console.log(Range.normalize(Range.parse('bytes=0-1024'), 5000))
|
|
154
|
+
// console.log(Range.normalize(Range.parse('bytes=0-0,-1'), 10000)) // 0 and 9999
|
|
155
|
+
// console.log(Range.normalize(Range.parse('bytes=0-1024, -1024'), 5000))
|
|
156
|
+
// console.log(Range.normalize(Range.parse('bytes= 0-999, 4500-5499, -1000'), 5000))
|
|
157
|
+
// console.log(Range.normalize(Range.parse('bytes=500-600,601-999'), 5000))
|
|
158
|
+
|
|
159
|
+
// console.log(Range.normalize(Range.parse('bytes=-500'), 10000)) // 9500-9999
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send_bytes } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
/** @import { EtagItem } from '../conditional.js' */
|
|
7
|
+
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
8
|
+
/** @import { SendBody } from './send-util.js' */
|
|
9
|
+
|
|
10
|
+
const { HTTP_STATUS_OK } = http2.constants
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {ServerHttp2Stream} stream
|
|
14
|
+
* @param {SendBody|undefined} obj
|
|
15
|
+
* @param {string|undefined} contentType
|
|
16
|
+
* @param {number|undefined} contentLength
|
|
17
|
+
* @param {string|undefined} encoding
|
|
18
|
+
* @param {EtagItem|undefined} etag
|
|
19
|
+
* @param {number|undefined} age
|
|
20
|
+
* @param {CacheControlOptions} cacheControl
|
|
21
|
+
* @param {'bytes'|'none'|undefined} acceptRanges
|
|
22
|
+
* @param {Metadata} meta
|
|
23
|
+
*/
|
|
24
|
+
export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, age, cacheControl, acceptRanges, meta) {
|
|
25
|
+
send_bytes(stream, HTTP_STATUS_OK, contentType, obj, undefined, contentLength, encoding, etag, age, cacheControl, acceptRanges, undefined, meta)
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const { HTTP_STATUS_PAYLOAD_TOO_LARGE } = http2.constants
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {ServerHttp2Stream} stream
|
|
11
|
+
* @param {Metadata} meta
|
|
12
|
+
*/
|
|
13
|
+
export function sendContentTooLarge(stream, meta) {
|
|
14
|
+
send(stream, HTTP_STATUS_PAYLOAD_TOO_LARGE, {}, [], undefined, undefined, meta)
|
|
15
|
+
}
|
package/src/response/defs.js
CHANGED
|
@@ -9,21 +9,33 @@ export const HTTP_HEADER_SEC_FETCH_SITE = 'sec-fetch-site'
|
|
|
9
9
|
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
|
+
export const HTTP_HEADER_ACCEPT_PATCH = 'accept-patch'
|
|
13
|
+
|
|
14
|
+
export const HTTP_METHOD_QUERY = 'QUERY'
|
|
15
|
+
export const HTTP_HEADER_ACCEPT_QUERY = 'accept-query'
|
|
12
16
|
|
|
13
17
|
export const DEFAULT_METHODS = [ 'HEAD', 'GET', 'POST', 'PATCH', 'DELETE' ]
|
|
14
18
|
|
|
15
19
|
export const HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE = 'access-control-max-age'
|
|
16
20
|
export const PREFLIGHT_AGE_SECONDS = '500'
|
|
17
21
|
|
|
22
|
+
/** @type {'bytes'} */
|
|
23
|
+
export const RANGE_UNITS_BYTES = 'bytes'
|
|
24
|
+
/** @type {'none'} */
|
|
25
|
+
export const RANGE_UNITS_NONE = 'none'
|
|
18
26
|
|
|
19
27
|
/** @import { TimingsInfo } from '../server-timing.js' */
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {`X-${string}`} CustomHeaderKey
|
|
31
|
+
*/
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* @typedef {Object} Metadata
|
|
23
35
|
* @property {Array<TimingsInfo>} performance
|
|
24
36
|
* @property {string|undefined} servername
|
|
25
37
|
* @property {string|undefined} origin
|
|
26
|
-
* @property {Array<[
|
|
38
|
+
* @property {Array<[ CustomHeaderKey, string ]>} customHeaders
|
|
27
39
|
*/
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const { HTTP_STATUS_FORBIDDEN} = http2.constants
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {ServerHttp2Stream} stream
|
|
11
|
+
* @param {Metadata} meta
|
|
12
|
+
*/
|
|
13
|
+
export function sendForbidden(stream, meta) {
|
|
14
|
+
throw new Error('unsupported')
|
|
15
|
+
send(stream, HTTP_STATUS_FORBIDDEN, {
|
|
16
|
+
}, [], undefined, undefined, meta)
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const { HTTP_STATUS_GONE } = http2.constants
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {ServerHttp2Stream} stream
|
|
11
|
+
* @param {Metadata} meta
|
|
12
|
+
*/
|
|
13
|
+
export function sendGone(stream, meta) {
|
|
14
|
+
send(stream, HTTP_STATUS_GONE, {}, [], undefined, undefined, meta)
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const { HTTP_STATUS_TEAPOT } = http2.constants
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {ServerHttp2Stream} stream
|
|
11
|
+
* @param {Metadata} meta
|
|
12
|
+
*/
|
|
13
|
+
export function sendImATeapot(stream, meta) {
|
|
14
|
+
send(stream, HTTP_STATUS_TEAPOT, {}, [], undefined, undefined, meta)
|
|
15
|
+
}
|
package/src/response/index.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
export * from './defs.js'
|
|
2
|
+
export * from './send-util.js'
|
|
2
3
|
|
|
3
4
|
export * from './accepted.js'
|
|
5
|
+
export * from './bytes.js'
|
|
4
6
|
export * from './conflict.js'
|
|
7
|
+
export * from './content-too-large.js'
|
|
5
8
|
export * from './created.js'
|
|
6
9
|
export * from './error.js'
|
|
10
|
+
export * from './forbidden.js'
|
|
11
|
+
export * from './gone.js'
|
|
12
|
+
export * from './im-a-teapot.js'
|
|
13
|
+
export * from './insufficient-storage.js'
|
|
7
14
|
export * from './json.js'
|
|
15
|
+
export * from './moved-permanently.js'
|
|
16
|
+
export * from './multiple-choices.js'
|
|
8
17
|
export * from './no-content.js'
|
|
9
18
|
export * from './not-acceptable.js'
|
|
10
19
|
export * from './not-allowed.js'
|
|
11
20
|
export * from './not-found.js'
|
|
12
21
|
export * from './not-implemented.js'
|
|
13
22
|
export * from './not-modified.js'
|
|
23
|
+
export * from './partial-content.js'
|
|
24
|
+
export * from './permanent-redirect.js'
|
|
14
25
|
export * from './precondition-failed.js'
|
|
15
26
|
export * from './preflight.js'
|
|
27
|
+
export * from './range-not-satisfiable.js'
|
|
28
|
+
export * from './see-other.js'
|
|
16
29
|
export * from './sse.js'
|
|
30
|
+
export * from './temporary-redirect.js'
|
|
17
31
|
export * from './timeout.js'
|
|
18
32
|
export * from './too-many-requests.js'
|
|
19
33
|
export * from './trace.js'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const { HTTP_STATUS_INSUFFICIENT_STORAGE } = http2.constants
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {ServerHttp2Stream} stream
|
|
11
|
+
* @param {Metadata} meta
|
|
12
|
+
*/
|
|
13
|
+
export function sendInsufficientStorage(stream, meta) {
|
|
14
|
+
send(stream, HTTP_STATUS_INSUFFICIENT_STORAGE, {}, [], undefined, undefined, meta)
|
|
15
|
+
}
|
package/src/response/json.js
CHANGED
|
@@ -1,43 +1,15 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
gzipSync,
|
|
6
|
-
zstdCompressSync
|
|
7
|
-
} from 'node:zlib'
|
|
8
|
-
import {
|
|
9
|
-
CHARSET_UTF8,
|
|
10
|
-
CONTENT_TYPE_JSON
|
|
11
|
-
} from '../content-type.js'
|
|
12
|
-
import { send } from './send-util.js'
|
|
13
|
-
import { Conditional } from '../conditional.js'
|
|
14
|
-
import { CacheControl } from '../cache-control.js'
|
|
2
|
+
|
|
3
|
+
import { send_encoded } from './send-util.js'
|
|
4
|
+
import { CONTENT_TYPE_JSON } from '../content-type.js'
|
|
15
5
|
|
|
16
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
17
7
|
/** @import { Metadata } from './defs.js' */
|
|
18
8
|
/** @import { EtagItem } from '../conditional.js' */
|
|
19
9
|
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
20
10
|
|
|
21
|
-
/** @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun */
|
|
22
|
-
|
|
23
|
-
const {
|
|
24
|
-
HTTP2_HEADER_CONTENT_ENCODING,
|
|
25
|
-
HTTP2_HEADER_VARY,
|
|
26
|
-
HTTP2_HEADER_CACHE_CONTROL,
|
|
27
|
-
HTTP2_HEADER_ETAG,
|
|
28
|
-
HTTP2_HEADER_AGE
|
|
29
|
-
} = http2.constants
|
|
30
|
-
|
|
31
11
|
const { HTTP_STATUS_OK } = http2.constants
|
|
32
12
|
|
|
33
|
-
/** @type {Map<string, EncoderFun>} */
|
|
34
|
-
export const ENCODER_MAP = new Map([
|
|
35
|
-
[ 'br', (data, charset) => brotliCompressSync(Buffer.from(data, charset)) ],
|
|
36
|
-
[ 'gzip', (data, charset) => gzipSync(Buffer.from(data, charset)) ],
|
|
37
|
-
[ 'deflate', (data, charset) => deflateSync(Buffer.from(data, charset)) ],
|
|
38
|
-
[ 'zstd', (data, charset) => zstdCompressSync(Buffer.from(data, charset)) ]
|
|
39
|
-
])
|
|
40
|
-
|
|
41
13
|
/**
|
|
42
14
|
* @param {ServerHttp2Stream} stream
|
|
43
15
|
* @param {Object} obj
|
|
@@ -45,31 +17,12 @@ export const ENCODER_MAP = new Map([
|
|
|
45
17
|
* @param {EtagItem|undefined} etag
|
|
46
18
|
* @param {number|undefined} age
|
|
47
19
|
* @param {CacheControlOptions} cacheControl
|
|
20
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
48
21
|
* @param {Metadata} meta
|
|
49
22
|
*/
|
|
50
|
-
export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, meta) {
|
|
23
|
+
export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, supportedQueryTypes, meta) {
|
|
51
24
|
if(stream.closed) { return }
|
|
52
25
|
|
|
53
26
|
const json = JSON.stringify(obj)
|
|
54
|
-
|
|
55
|
-
const useIdentity = encoding === 'identity'
|
|
56
|
-
const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
|
|
57
|
-
const hasEncoder = encoder !== undefined
|
|
58
|
-
const actualEncoding = hasEncoder ? encoding : undefined
|
|
59
|
-
|
|
60
|
-
const encodeStart = performance.now()
|
|
61
|
-
const encodedData = hasEncoder && !useIdentity ? encoder(json, CHARSET_UTF8) : json
|
|
62
|
-
const encodeEnd = performance.now()
|
|
63
|
-
|
|
64
|
-
meta.performance.push(
|
|
65
|
-
{ name: 'encode', duration: encodeEnd - encodeStart }
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
send(stream, HTTP_STATUS_OK, {
|
|
69
|
-
[HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
|
|
70
|
-
[HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
|
|
71
|
-
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
72
|
-
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
73
|
-
[HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
|
|
74
|
-
}, [ HTTP2_HEADER_AGE ], CONTENT_TYPE_JSON, encodedData, meta)
|
|
27
|
+
send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, age, cacheControl, undefined, supportedQueryTypes, meta)
|
|
75
28
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
HTTP2_HEADER_LOCATION
|
|
9
|
+
} = http2.constants
|
|
10
|
+
|
|
11
|
+
const { HTTP_STATUS_MOVED_PERMANENTLY } = http2.constants
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ServerHttp2Stream} stream
|
|
15
|
+
* @param {URL} location
|
|
16
|
+
* @param {Metadata} meta
|
|
17
|
+
*/
|
|
18
|
+
export function sendMovedPermanently(stream, location, meta) {
|
|
19
|
+
send(stream, HTTP_STATUS_MOVED_PERMANENTLY, {
|
|
20
|
+
[HTTP2_HEADER_LOCATION]: location.href
|
|
21
|
+
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const { HTTP_STATUS_MULTIPLE_CHOICES } = http2.constants
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {ServerHttp2Stream} stream
|
|
11
|
+
* @param {Metadata} meta
|
|
12
|
+
*/
|
|
13
|
+
export function sendMultipleChoices(stream, meta) {
|
|
14
|
+
throw new Error('unsupported')
|
|
15
|
+
send(stream, HTTP_STATUS_MULTIPLE_CHOICES, {
|
|
16
|
+
// Alternates:
|
|
17
|
+
// TCN: list
|
|
18
|
+
// Vary: negotiate
|
|
19
|
+
}, [], undefined, undefined, meta)
|
|
20
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
|
|
3
|
+
import { send_bytes } from './send-util.js'
|
|
4
|
+
import { RANGE_UNITS_BYTES } from "./defs.js"
|
|
5
|
+
import { Multipart } from '../multipart.js'
|
|
6
|
+
import { MIME_TYPE_MULTIPART_RANGE } from '../content-type.js'
|
|
7
|
+
|
|
8
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
9
|
+
/** @import { Metadata } from './defs.js' */
|
|
10
|
+
/** @import { EtagItem } from '../conditional.js' */
|
|
11
|
+
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
12
|
+
/** @import { ContentRangeDirective } from '../content-range.js' */
|
|
13
|
+
/** @import { SendBody } from './send-util.js' */
|
|
14
|
+
|
|
15
|
+
const { HTTP_STATUS_PARTIAL_CONTENT } = http2.constants
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @template T
|
|
19
|
+
* @typedef {[ T, ...T[] ]} NonEmptyArray
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} PartialBytes
|
|
24
|
+
* @property {SendBody} obj
|
|
25
|
+
* @property {ContentRangeDirective} range
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {ServerHttp2Stream} stream
|
|
30
|
+
* @param {string} contentType
|
|
31
|
+
* @param {NonEmptyArray<PartialBytes>|PartialBytes} objs
|
|
32
|
+
* @param {number|undefined} contentLength
|
|
33
|
+
* @param {string|undefined} encoding
|
|
34
|
+
* @param {EtagItem|undefined} etag
|
|
35
|
+
* @param {number|undefined} age
|
|
36
|
+
* @param {CacheControlOptions} cacheControl
|
|
37
|
+
* @param {Metadata} meta
|
|
38
|
+
*/
|
|
39
|
+
export function sendPartialContent(stream, contentType, objs, contentLength, encoding, etag, age, cacheControl, meta) {
|
|
40
|
+
const acceptRanges = RANGE_UNITS_BYTES
|
|
41
|
+
const supportedQueryTypes = undefined
|
|
42
|
+
|
|
43
|
+
if(Array.isArray(objs) && objs.length > 1) {
|
|
44
|
+
// send using multipart bytes
|
|
45
|
+
const boundary = 'PARTIAL_CONTENT_BOUNDARY' // todo make unique for content
|
|
46
|
+
const obj = Multipart.encode_Bytes(contentType, objs, contentLength, boundary)
|
|
47
|
+
|
|
48
|
+
const multipartContentType = `${MIME_TYPE_MULTIPART_RANGE}; boundary=${boundary}`
|
|
49
|
+
|
|
50
|
+
send_bytes(
|
|
51
|
+
stream,
|
|
52
|
+
HTTP_STATUS_PARTIAL_CONTENT,
|
|
53
|
+
multipartContentType,
|
|
54
|
+
obj,
|
|
55
|
+
undefined,
|
|
56
|
+
undefined,
|
|
57
|
+
encoding,
|
|
58
|
+
etag,
|
|
59
|
+
age,
|
|
60
|
+
cacheControl,
|
|
61
|
+
acceptRanges,
|
|
62
|
+
supportedQueryTypes,
|
|
63
|
+
meta)
|
|
64
|
+
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// single range, send as regular object
|
|
69
|
+
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)
|
|
71
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
HTTP2_HEADER_LOCATION
|
|
9
|
+
} = http2.constants
|
|
10
|
+
|
|
11
|
+
const { HTTP_STATUS_PERMANENT_REDIRECT } = http2.constants
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ServerHttp2Stream} stream
|
|
15
|
+
* @param {URL} location
|
|
16
|
+
* @param {Metadata} meta
|
|
17
|
+
*/
|
|
18
|
+
export function sendPermanentRedirect(stream, location, meta) {
|
|
19
|
+
throw new Error('unsupported')
|
|
20
|
+
send(stream, HTTP_STATUS_PERMANENT_REDIRECT, {
|
|
21
|
+
[HTTP2_HEADER_LOCATION]: location.href
|
|
22
|
+
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
23
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
import {
|
|
3
3
|
HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
|
|
4
|
+
HTTP_HEADER_ACCEPT_QUERY,
|
|
5
|
+
HTTP_METHOD_QUERY,
|
|
4
6
|
PREFLIGHT_AGE_SECONDS
|
|
5
7
|
} from './defs.js'
|
|
6
8
|
import { send } from './send-util.js'
|
|
@@ -14,7 +16,10 @@ const {
|
|
|
14
16
|
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
|
|
15
17
|
HTTP2_HEADER_IF_MATCH,
|
|
16
18
|
HTTP2_HEADER_IF_NONE_MATCH,
|
|
17
|
-
HTTP2_HEADER_AUTHORIZATION
|
|
19
|
+
HTTP2_HEADER_AUTHORIZATION,
|
|
20
|
+
HTTP2_HEADER_ACCEPT_RANGES,
|
|
21
|
+
HTTP2_HEADER_RANGE,
|
|
22
|
+
HTTP2_HEADER_IF_RANGE
|
|
18
23
|
} = http2.constants
|
|
19
24
|
|
|
20
25
|
const { HTTP_STATUS_OK } = http2.constants
|
|
@@ -22,18 +27,28 @@ const { HTTP_STATUS_OK } = http2.constants
|
|
|
22
27
|
/**
|
|
23
28
|
* @param {ServerHttp2Stream} stream
|
|
24
29
|
* @param {Array<string>} methods
|
|
30
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
31
|
+
* @param {'byte'|'none'|undefined} acceptRanges
|
|
25
32
|
* @param {Metadata} meta
|
|
26
33
|
*/
|
|
27
|
-
export function sendPreflight(stream, methods, meta) {
|
|
34
|
+
export function sendPreflight(stream, methods, supportedQueryTypes, acceptRanges, meta) {
|
|
35
|
+
const supportsQuery = methods.includes(HTTP_METHOD_QUERY) && supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
36
|
+
const exposedHeadersAcceptQuery = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY ] : []
|
|
37
|
+
const exposedHeaders = acceptRanges !== undefined ? [ HTTP2_HEADER_ACCEPT_RANGES, ...exposedHeadersAcceptQuery ] : exposedHeadersAcceptQuery
|
|
38
|
+
|
|
28
39
|
send(stream, HTTP_STATUS_OK, {
|
|
29
40
|
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS]: methods.join(','),
|
|
30
41
|
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: [
|
|
31
42
|
HTTP2_HEADER_IF_MATCH,
|
|
32
43
|
HTTP2_HEADER_IF_NONE_MATCH,
|
|
33
44
|
HTTP2_HEADER_AUTHORIZATION,
|
|
34
|
-
HTTP2_HEADER_CONTENT_TYPE
|
|
45
|
+
HTTP2_HEADER_CONTENT_TYPE,
|
|
46
|
+
HTTP2_HEADER_RANGE,
|
|
47
|
+
HTTP2_HEADER_IF_RANGE
|
|
35
48
|
].join(','),
|
|
36
|
-
[HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS
|
|
49
|
+
[HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS,
|
|
50
|
+
[HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
|
|
51
|
+
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
|
|
37
52
|
// Access-Control-Allow-Credentials
|
|
38
|
-
},
|
|
53
|
+
}, exposedHeaders, undefined, undefined, meta)
|
|
39
54
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
import { CONTENT_RANGE_UNKNOWN, ContentRange } from '../content-range.js'
|
|
4
|
+
|
|
5
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
+
/** @import { Metadata } from './defs.js' */
|
|
7
|
+
/** @import { ContentRangeDirective} from '../content-range.js' */
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
HTTP2_HEADER_CONTENT_RANGE
|
|
11
|
+
} = http2.constants
|
|
12
|
+
|
|
13
|
+
const { HTTP_STATUS_RANGE_NOT_SATISFIABLE } = http2.constants
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {ServerHttp2Stream} stream
|
|
17
|
+
* @param {ContentRangeDirective} rangeDirective
|
|
18
|
+
* @param {Metadata} meta
|
|
19
|
+
*/
|
|
20
|
+
export function sendRangeNotSatisfiable(stream, rangeDirective, meta) {
|
|
21
|
+
/** @type {ContentRangeDirective} */
|
|
22
|
+
const invalidRange = { size: rangeDirective.size, range: CONTENT_RANGE_UNKNOWN }
|
|
23
|
+
|
|
24
|
+
send(stream, HTTP_STATUS_RANGE_NOT_SATISFIABLE, {
|
|
25
|
+
[HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(invalidRange)
|
|
26
|
+
}, [ HTTP2_HEADER_CONTENT_RANGE ], undefined, undefined, meta)
|
|
27
|
+
}
|