@johntalton/http-util 3.0.0 → 4.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/package.json +1 -1
- package/src/body.js +1 -1
- package/src/cache-control.js +57 -0
- package/src/conditional.js +54 -1
- package/src/response/header-util.js +2 -0
- package/src/response/json.js +10 -5
- package/src/response/not-modified.js +5 -2
- package/src/response/preflight.js +11 -2
package/package.json
CHANGED
package/src/body.js
CHANGED
|
@@ -13,7 +13,7 @@ export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {Object} BodyOptions
|
|
16
|
-
* @property {AbortSignal} [signal]
|
|
16
|
+
* @property {AbortSignal|undefined} [signal]
|
|
17
17
|
* @property {number} [byteLimit]
|
|
18
18
|
* @property {number} [contentLength]
|
|
19
19
|
* @property {ContentType|undefined} [contentType]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** @typedef {'no-cache'|'no-store'|'no-transform'|'must-revalidate'|'immutable'|'must-understand'} Directives */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} CacheControlOptions
|
|
5
|
+
* @property {boolean|undefined} [priv]
|
|
6
|
+
* @property {boolean|undefined} [pub]
|
|
7
|
+
* @property {number|undefined} [maxAge]
|
|
8
|
+
* @property {number|undefined} [staleWhileRevalidate]
|
|
9
|
+
* @property {number|undefined} [staleIfError]
|
|
10
|
+
* @property {Directives|Array<Directives>|undefined} [directives]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class CacheControl {
|
|
14
|
+
/**
|
|
15
|
+
* @param {CacheControlOptions} options
|
|
16
|
+
* @returns {string|undefined}
|
|
17
|
+
*/
|
|
18
|
+
static encode(options) {
|
|
19
|
+
const {
|
|
20
|
+
pub,
|
|
21
|
+
priv,
|
|
22
|
+
maxAge,
|
|
23
|
+
directives,
|
|
24
|
+
staleWhileRevalidate,
|
|
25
|
+
staleIfError
|
|
26
|
+
} = options
|
|
27
|
+
|
|
28
|
+
const result = []
|
|
29
|
+
|
|
30
|
+
if(pub !== undefined && pub && !priv) { result.push('public') }
|
|
31
|
+
if(priv !== undefined && priv && !pub) { result.push('private') }
|
|
32
|
+
|
|
33
|
+
if(directives !== undefined) {
|
|
34
|
+
result.push(...(Array.isArray(directives) ? directives : [ directives ]))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if(maxAge !== undefined && Number.isInteger(maxAge) && maxAge >= 0) {
|
|
38
|
+
result.push(`max-age=${maxAge}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if(staleWhileRevalidate !== undefined && Number.isInteger(staleWhileRevalidate) && staleWhileRevalidate >= 0) {
|
|
42
|
+
result.push(`stale-while-revalidate=${staleWhileRevalidate}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if(staleIfError !== undefined && Number.isInteger(staleIfError) && staleIfError >= 0) {
|
|
46
|
+
result.push(`stale-if-error=${staleIfError}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result.join(', ')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// console.log(CacheControl.encode({
|
|
54
|
+
// priv: true,
|
|
55
|
+
// maxAge: 60,
|
|
56
|
+
// directives: ['must-revalidate', 'no-transform']
|
|
57
|
+
// }))
|
package/src/conditional.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* @property {number} hour
|
|
32
32
|
* @property {number} minute
|
|
33
33
|
* @property {number} second
|
|
34
|
-
* @property {Date} date
|
|
34
|
+
* @property {Date|undefined} [date]
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
export const CONDITION_ETAG_SEPARATOR = ','
|
|
@@ -79,6 +79,31 @@ export function isQuoted(etag) {
|
|
|
79
79
|
return true
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export class ETag {
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} etag
|
|
85
|
+
* @returns {WeakEtagItem}
|
|
86
|
+
*/
|
|
87
|
+
static weak(etag) {
|
|
88
|
+
if(!isValidEtag(etag)) { throw new Error('invalid etag format') }
|
|
89
|
+
return { any: false, weak: true, etag }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} etag
|
|
94
|
+
* @returns {NotWeakEtagItem}
|
|
95
|
+
*/
|
|
96
|
+
static strong(etag) {
|
|
97
|
+
if(!isValidEtag(etag)) { throw new Error('invalid etag format') }
|
|
98
|
+
return { any: false, weak: false, etag }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @returns {AnyEtagItem}
|
|
103
|
+
*/
|
|
104
|
+
static any() { return ANY_ETAG_ITEM }
|
|
105
|
+
}
|
|
106
|
+
|
|
82
107
|
export class Conditional {
|
|
83
108
|
/**
|
|
84
109
|
* @param {EtagItem|undefined} etagItem
|
|
@@ -143,6 +168,34 @@ export class Conditional {
|
|
|
143
168
|
.filter(item => item !== undefined)
|
|
144
169
|
}
|
|
145
170
|
|
|
171
|
+
/**
|
|
172
|
+
* @param {Array<EtagItem>} etagItemList
|
|
173
|
+
*/
|
|
174
|
+
static hasAny(etagItemList) {
|
|
175
|
+
return etagItemList.find(item => item.any) !== undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {Array<EtagItem>} etagItemList
|
|
180
|
+
* @param {string} etag
|
|
181
|
+
*/
|
|
182
|
+
static hasEtag(etagItemList, etag) {
|
|
183
|
+
return etagItemList.find(item => item.etag === etag) !== undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {IMFFixDate|undefined} fixDate
|
|
188
|
+
* @returns {string|undefined}
|
|
189
|
+
*/
|
|
190
|
+
static encodeFixDate(fixDate) {
|
|
191
|
+
if(fixDate === undefined) { return undefined }
|
|
192
|
+
if(fixDate.date !== undefined) { return fixDate.date.toUTCString() }
|
|
193
|
+
|
|
194
|
+
const { year, month, day, hour, minute, second } = fixDate
|
|
195
|
+
const d = new Date(Date.UTC(year, DATE_MONTHS.indexOf(month), day, hour, minute, second))
|
|
196
|
+
return d.toUTCString()
|
|
197
|
+
}
|
|
198
|
+
|
|
146
199
|
/**
|
|
147
200
|
* @param {String|string|undefined} matchHeader
|
|
148
201
|
* @returns {IMFFixDate|undefined}
|
|
@@ -24,6 +24,8 @@ const {
|
|
|
24
24
|
export function coreHeaders(status, contentType, meta) {
|
|
25
25
|
return {
|
|
26
26
|
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
27
|
+
// Access-Control-Expose-Headers
|
|
28
|
+
// Access-Control-Allow-Credentials // for non-preflight
|
|
27
29
|
[HTTP2_HEADER_STATUS]: status,
|
|
28
30
|
[HTTP2_HEADER_CONTENT_TYPE]: contentType,
|
|
29
31
|
[HTTP2_HEADER_SERVER]: meta.servername
|
package/src/response/json.js
CHANGED
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
} from '../content-type.js'
|
|
12
12
|
import { send } from './send-util.js'
|
|
13
13
|
import { Conditional } from '../conditional.js'
|
|
14
|
+
import { CacheControl } from '../cache-control.js'
|
|
14
15
|
|
|
15
16
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
16
17
|
/** @import { Metadata } from './defs.js' */
|
|
17
18
|
/** @import { EtagItem } from '../conditional.js' */
|
|
19
|
+
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
18
20
|
|
|
19
21
|
/** @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun */
|
|
20
22
|
|
|
@@ -22,7 +24,8 @@ const {
|
|
|
22
24
|
HTTP2_HEADER_CONTENT_ENCODING,
|
|
23
25
|
HTTP2_HEADER_VARY,
|
|
24
26
|
HTTP2_HEADER_CACHE_CONTROL,
|
|
25
|
-
HTTP2_HEADER_ETAG
|
|
27
|
+
HTTP2_HEADER_ETAG,
|
|
28
|
+
HTTP2_HEADER_AGE
|
|
26
29
|
} = http2.constants
|
|
27
30
|
|
|
28
31
|
const { HTTP_STATUS_OK } = http2.constants
|
|
@@ -40,9 +43,11 @@ export const ENCODER_MAP = new Map([
|
|
|
40
43
|
* @param {Object} obj
|
|
41
44
|
* @param {string|undefined} encoding
|
|
42
45
|
* @param {EtagItem|undefined} etag
|
|
46
|
+
* @param {number|undefined} age
|
|
47
|
+
* @param {CacheControlOptions} cacheControl
|
|
43
48
|
* @param {Metadata} meta
|
|
44
49
|
*/
|
|
45
|
-
export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
|
|
50
|
+
export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, meta) {
|
|
46
51
|
if(stream.closed) { return }
|
|
47
52
|
|
|
48
53
|
const json = JSON.stringify(obj)
|
|
@@ -63,8 +68,8 @@ export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
|
|
|
63
68
|
send(stream, HTTP_STATUS_OK, {
|
|
64
69
|
[HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
|
|
65
70
|
[HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
|
|
66
|
-
[HTTP2_HEADER_CACHE_CONTROL]:
|
|
67
|
-
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
68
|
-
|
|
71
|
+
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
72
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
73
|
+
[HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
|
|
69
74
|
}, CONTENT_TYPE_JSON, encodedData, meta)
|
|
70
75
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
import { send } from './send-util.js'
|
|
3
3
|
import { Conditional } from '../conditional.js'
|
|
4
|
+
import { CacheControl } from '../cache-control.js'
|
|
4
5
|
|
|
5
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
7
|
/** @import { Metadata } from './defs.js' */
|
|
7
8
|
/** @import { EtagItem } from '../conditional.js' */
|
|
9
|
+
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
8
10
|
|
|
9
11
|
const {
|
|
10
12
|
HTTP2_HEADER_AGE,
|
|
@@ -19,12 +21,13 @@ const { HTTP_STATUS_NOT_MODIFIED } = http2.constants
|
|
|
19
21
|
* @param {ServerHttp2Stream} stream
|
|
20
22
|
* @param {EtagItem|undefined} etag
|
|
21
23
|
* @param {number|undefined} age
|
|
24
|
+
* @param {CacheControlOptions} cacheControl
|
|
22
25
|
* @param {Metadata} meta
|
|
23
26
|
*/
|
|
24
|
-
export function sendNotModified(stream, etag, age, meta) {
|
|
27
|
+
export function sendNotModified(stream, etag, age, cacheControl, meta) {
|
|
25
28
|
send(stream, HTTP_STATUS_NOT_MODIFIED, {
|
|
26
29
|
[HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
|
|
27
|
-
[HTTP2_HEADER_CACHE_CONTROL]:
|
|
30
|
+
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
28
31
|
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
29
32
|
[HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
|
|
30
33
|
}, undefined, undefined, meta)
|
|
@@ -11,7 +11,10 @@ import { send } from './send-util.js'
|
|
|
11
11
|
const {
|
|
12
12
|
HTTP2_HEADER_CONTENT_TYPE,
|
|
13
13
|
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
|
|
14
|
-
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
|
|
14
|
+
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
|
|
15
|
+
HTTP2_HEADER_IF_MATCH,
|
|
16
|
+
HTTP2_HEADER_IF_NONE_MATCH,
|
|
17
|
+
HTTP2_HEADER_AUTHORIZATION
|
|
15
18
|
} = http2.constants
|
|
16
19
|
|
|
17
20
|
const { HTTP_STATUS_OK } = http2.constants
|
|
@@ -24,7 +27,13 @@ const { HTTP_STATUS_OK } = http2.constants
|
|
|
24
27
|
export function sendPreflight(stream, methods, meta) {
|
|
25
28
|
send(stream, HTTP_STATUS_OK, {
|
|
26
29
|
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS]: methods.join(','),
|
|
27
|
-
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: [
|
|
30
|
+
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: [
|
|
31
|
+
HTTP2_HEADER_IF_MATCH,
|
|
32
|
+
HTTP2_HEADER_IF_NONE_MATCH,
|
|
33
|
+
HTTP2_HEADER_AUTHORIZATION,
|
|
34
|
+
HTTP2_HEADER_CONTENT_TYPE
|
|
35
|
+
].join(','),
|
|
28
36
|
[HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS
|
|
37
|
+
// Access-Control-Allow-Credentials
|
|
29
38
|
}, undefined, undefined, meta)
|
|
30
39
|
}
|