@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/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
+ }
@@ -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<[ string, string ]>} customHeaders
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
+ }
@@ -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
+ }
@@ -1,43 +1,15 @@
1
1
  import http2 from 'node:http2'
2
- import {
3
- brotliCompressSync,
4
- deflateSync,
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
- }, [], undefined, undefined, meta)
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
+ }