@johntalton/http-util 2.0.5 → 3.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/conditional.js +77 -25
- package/src/response/created.js +8 -3
- package/src/response/defs.js +0 -1
- package/src/response/json.js +5 -2
- package/src/response/no-content.js +5 -2
- package/src/response/not-modified.js +7 -4
- package/src/response/too-many-requests.js +5 -4
package/package.json
CHANGED
package/src/conditional.js
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} WeakEtagItem
|
|
4
|
+
* @property {true} weak
|
|
5
|
+
* @property {false} any
|
|
6
|
+
* @property {string} etag
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} AnyEtagItem
|
|
11
|
+
* @property {boolean} weak
|
|
12
|
+
* @property {true} any
|
|
13
|
+
* @property {'*'} etag
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} NotWeakEtagItem
|
|
18
|
+
* @property {false} weak
|
|
19
|
+
* @property {false} any
|
|
20
|
+
* @property {string} etag
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @typedef {WeakEtagItem | NotWeakEtagItem | AnyEtagItem } EtagItem */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} IMFFixDate
|
|
27
|
+
* @property {typeof DATE_DAYS[number]} dayName
|
|
28
|
+
* @property {number} day
|
|
29
|
+
* @property {typeof DATE_MONTHS[number]} month
|
|
30
|
+
* @property {number} year
|
|
31
|
+
* @property {number} hour
|
|
32
|
+
* @property {number} minute
|
|
33
|
+
* @property {number} second
|
|
34
|
+
* @property {Date} date
|
|
35
|
+
*/
|
|
36
|
+
|
|
1
37
|
export const CONDITION_ETAG_SEPARATOR = ','
|
|
2
38
|
export const CONDITION_ETAG_ANY = '*'
|
|
3
39
|
export const CONDITION_ETAG_WEAK_PREFIX = 'W/'
|
|
4
40
|
export const ETAG_QUOTE = '"'
|
|
5
41
|
|
|
42
|
+
/** @type {AnyEtagItem} */
|
|
43
|
+
export const ANY_ETAG_ITEM = { any: true, weak: false, etag: CONDITION_ETAG_ANY }
|
|
44
|
+
|
|
6
45
|
export const DATE_SPACE = ' '
|
|
7
46
|
export const DATE_DAYS = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ]
|
|
8
47
|
export const DATE_MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
|
|
@@ -40,26 +79,25 @@ export function isQuoted(etag) {
|
|
|
40
79
|
return true
|
|
41
80
|
}
|
|
42
81
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
82
|
+
export class Conditional {
|
|
83
|
+
/**
|
|
84
|
+
* @param {EtagItem|undefined} etagItem
|
|
85
|
+
* @returns {string|undefined}
|
|
86
|
+
*/
|
|
87
|
+
static encodeEtag(etagItem) {
|
|
88
|
+
if(etagItem === undefined) { return undefined }
|
|
89
|
+
if(etagItem.any) {
|
|
90
|
+
if(etagItem.etag !== CONDITION_ETAG_ANY) { return undefined }
|
|
91
|
+
return CONDITION_ETAG_ANY
|
|
92
|
+
}
|
|
49
93
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* @property {number} hour
|
|
57
|
-
* @property {number} minute
|
|
58
|
-
* @property {number} second
|
|
59
|
-
* @property {Date} date
|
|
60
|
-
*/
|
|
94
|
+
if(etagItem.etag === CONDITION_ETAG_ANY) { return undefined }
|
|
95
|
+
if(!isValidEtag(etagItem.etag)) { return undefined }
|
|
96
|
+
|
|
97
|
+
const prefix = etagItem.weak ? CONDITION_ETAG_WEAK_PREFIX : ''
|
|
98
|
+
return `${prefix}${ETAG_QUOTE}${etagItem.etag}${ETAG_QUOTE}`
|
|
99
|
+
}
|
|
61
100
|
|
|
62
|
-
export class Conditional {
|
|
63
101
|
/**
|
|
64
102
|
* @param {string|undefined} matchHeader
|
|
65
103
|
* @returns {Array<EtagItem>}
|
|
@@ -85,12 +123,7 @@ export class Conditional {
|
|
|
85
123
|
}
|
|
86
124
|
})
|
|
87
125
|
.map(item => {
|
|
88
|
-
if(item.etag === CONDITION_ETAG_ANY) {
|
|
89
|
-
return {
|
|
90
|
-
...item,
|
|
91
|
-
any: true
|
|
92
|
-
}
|
|
93
|
-
}
|
|
126
|
+
if(item.etag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
|
|
94
127
|
|
|
95
128
|
// validated quoted
|
|
96
129
|
if(!isQuoted(item.etag)) { return undefined }
|
|
@@ -98,11 +131,14 @@ export class Conditional {
|
|
|
98
131
|
if(!isValidEtag(etag)) { return undefined }
|
|
99
132
|
if(etag === CONDITION_ETAG_ANY) { return undefined }
|
|
100
133
|
|
|
101
|
-
|
|
134
|
+
/** @type {WeakEtagItem | NotWeakEtagItem} */
|
|
135
|
+
const result = {
|
|
102
136
|
weak: item.weak,
|
|
103
137
|
any: false,
|
|
104
138
|
etag
|
|
105
139
|
}
|
|
140
|
+
|
|
141
|
+
return result
|
|
106
142
|
})
|
|
107
143
|
.filter(item => item !== undefined)
|
|
108
144
|
}
|
|
@@ -187,6 +223,20 @@ export class Conditional {
|
|
|
187
223
|
}
|
|
188
224
|
}
|
|
189
225
|
|
|
226
|
+
// Ok
|
|
227
|
+
// console.log(Conditional.encodeEtag({ any: true, weak: false, etag: '*' }))
|
|
228
|
+
// console.log(Conditional.encodeEtag({ any: true, weak: true, etag: '*' }))
|
|
229
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo' }))
|
|
230
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: true, etag: 'WeakFoo' }))
|
|
231
|
+
|
|
232
|
+
// Error
|
|
233
|
+
// console.log(Conditional.encodeEtag(undefined))
|
|
234
|
+
// console.log(Conditional.encodeEtag({ any: true, weak: false, etag: 'NotAsterisk' }))
|
|
235
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo\tBar' }))
|
|
236
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo"Bar' }))
|
|
237
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: '*' }))
|
|
238
|
+
|
|
239
|
+
|
|
190
240
|
// Ok
|
|
191
241
|
// console.log(Conditional.parseEtagList('"bfc13a64729c4290ef5b2c2730249c88ca92d82d"'))
|
|
192
242
|
// console.log(Conditional.parseEtagList('W/"67ab43", "54ed21", "7892dd"'))
|
|
@@ -225,6 +275,8 @@ export class Conditional {
|
|
|
225
275
|
// }
|
|
226
276
|
// }
|
|
227
277
|
|
|
278
|
+
|
|
279
|
+
|
|
228
280
|
// const testBad = [
|
|
229
281
|
// undefined,
|
|
230
282
|
// null,
|
package/src/response/created.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
import { send } from './send-util.js'
|
|
3
|
+
import { Conditional } from '../conditional.js'
|
|
3
4
|
|
|
4
5
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
6
|
/** @import { Metadata } from './defs.js' */
|
|
7
|
+
/** @import { EtagItem } from '../conditional.js' */
|
|
6
8
|
|
|
7
9
|
const {
|
|
8
|
-
HTTP2_HEADER_LOCATION
|
|
10
|
+
HTTP2_HEADER_LOCATION,
|
|
11
|
+
HTTP2_HEADER_ETAG
|
|
9
12
|
} = http2.constants
|
|
10
13
|
|
|
11
14
|
const { HTTP_STATUS_CREATED } = http2.constants
|
|
@@ -13,10 +16,12 @@ const { HTTP_STATUS_CREATED } = http2.constants
|
|
|
13
16
|
/**
|
|
14
17
|
* @param {ServerHttp2Stream} stream
|
|
15
18
|
* @param {URL} location
|
|
19
|
+
* @param {EtagItem|undefined} etag
|
|
16
20
|
* @param {Metadata} meta
|
|
17
21
|
*/
|
|
18
|
-
export function sendCreated(stream, location, meta) {
|
|
22
|
+
export function sendCreated(stream, location, etag, meta) {
|
|
19
23
|
send(stream, HTTP_STATUS_CREATED, {
|
|
20
|
-
[HTTP2_HEADER_LOCATION]: location.href
|
|
24
|
+
[HTTP2_HEADER_LOCATION]: location.href,
|
|
25
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
21
26
|
}, undefined, undefined, meta)
|
|
22
27
|
}
|
package/src/response/defs.js
CHANGED
package/src/response/json.js
CHANGED
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
CONTENT_TYPE_JSON
|
|
11
11
|
} from '../content-type.js'
|
|
12
12
|
import { send } from './send-util.js'
|
|
13
|
+
import { Conditional } from '../conditional.js'
|
|
13
14
|
|
|
14
15
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
15
16
|
/** @import { Metadata } from './defs.js' */
|
|
17
|
+
/** @import { EtagItem } from '../conditional.js' */
|
|
16
18
|
|
|
17
19
|
/** @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun */
|
|
18
20
|
|
|
@@ -37,9 +39,10 @@ export const ENCODER_MAP = new Map([
|
|
|
37
39
|
* @param {ServerHttp2Stream} stream
|
|
38
40
|
* @param {Object} obj
|
|
39
41
|
* @param {string|undefined} encoding
|
|
42
|
+
* @param {EtagItem|undefined} etag
|
|
40
43
|
* @param {Metadata} meta
|
|
41
44
|
*/
|
|
42
|
-
export function sendJSON_Encoded(stream, obj, encoding, meta) {
|
|
45
|
+
export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
|
|
43
46
|
if(stream.closed) { return }
|
|
44
47
|
|
|
45
48
|
const json = JSON.stringify(obj)
|
|
@@ -61,7 +64,7 @@ export function sendJSON_Encoded(stream, obj, encoding, meta) {
|
|
|
61
64
|
[HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
|
|
62
65
|
[HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
|
|
63
66
|
[HTTP2_HEADER_CACHE_CONTROL]: 'private',
|
|
64
|
-
[HTTP2_HEADER_ETAG]:
|
|
67
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
65
68
|
// [HTTP2_HEADER_AGE]: age
|
|
66
69
|
}, CONTENT_TYPE_JSON, encodedData, meta)
|
|
67
70
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
import { send } from './send-util.js'
|
|
3
|
+
import { Conditional } from '../conditional.js'
|
|
3
4
|
|
|
4
5
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
6
|
/** @import { Metadata } from './defs.js' */
|
|
7
|
+
/** @import { EtagItem } from '../conditional.js' */
|
|
6
8
|
|
|
7
9
|
const {
|
|
8
10
|
HTTP2_HEADER_ETAG
|
|
@@ -12,10 +14,11 @@ const { HTTP_STATUS_NO_CONTENT } = http2.constants
|
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* @param {ServerHttp2Stream} stream
|
|
17
|
+
* @param {EtagItem|undefined} etag
|
|
15
18
|
* @param {Metadata} meta
|
|
16
19
|
*/
|
|
17
|
-
export function sendNoContent(stream, meta) {
|
|
20
|
+
export function sendNoContent(stream, etag, meta) {
|
|
18
21
|
send(stream, HTTP_STATUS_NO_CONTENT, {
|
|
19
|
-
[HTTP2_HEADER_ETAG]:
|
|
22
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
20
23
|
}, undefined, undefined, meta)
|
|
21
24
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
import { send } from './send-util.js'
|
|
3
|
+
import { Conditional } from '../conditional.js'
|
|
3
4
|
|
|
4
5
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
6
|
/** @import { Metadata } from './defs.js' */
|
|
7
|
+
/** @import { EtagItem } from '../conditional.js' */
|
|
6
8
|
|
|
7
9
|
const {
|
|
8
10
|
HTTP2_HEADER_AGE,
|
|
@@ -15,14 +17,15 @@ const { HTTP_STATUS_NOT_MODIFIED } = http2.constants
|
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* @param {ServerHttp2Stream} stream
|
|
18
|
-
* @param {
|
|
20
|
+
* @param {EtagItem|undefined} etag
|
|
21
|
+
* @param {number|undefined} age
|
|
19
22
|
* @param {Metadata} meta
|
|
20
23
|
*/
|
|
21
|
-
export function sendNotModified(stream, age, meta) {
|
|
24
|
+
export function sendNotModified(stream, etag, age, meta) {
|
|
22
25
|
send(stream, HTTP_STATUS_NOT_MODIFIED, {
|
|
23
26
|
[HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
|
|
24
27
|
[HTTP2_HEADER_CACHE_CONTROL]: 'private',
|
|
25
|
-
[HTTP2_HEADER_ETAG]:
|
|
26
|
-
[HTTP2_HEADER_AGE]: `${age}`
|
|
28
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
29
|
+
[HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
|
|
27
30
|
}, undefined, undefined, meta)
|
|
28
31
|
}
|
|
@@ -10,6 +10,7 @@ import { send } from './send-util.js'
|
|
|
10
10
|
|
|
11
11
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
12
12
|
/** @import { Metadata } from './defs.js' */
|
|
13
|
+
/** @import { RateLimitInfo, RateLimitPolicyInfo } from '../rate-limit.js' */
|
|
13
14
|
|
|
14
15
|
const {
|
|
15
16
|
HTTP2_HEADER_RETRY_AFTER
|
|
@@ -19,17 +20,17 @@ const { HTTP_STATUS_TOO_MANY_REQUESTS } = http2.constants
|
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* @param {ServerHttp2Stream} stream
|
|
22
|
-
* @param {
|
|
23
|
-
* @param {Array<
|
|
23
|
+
* @param {RateLimitInfo} limitInfo
|
|
24
|
+
* @param {Array<RateLimitPolicyInfo>} policies
|
|
24
25
|
* @param {Metadata} meta
|
|
25
26
|
*/
|
|
26
27
|
export function sendTooManyRequests(stream, limitInfo, policies, meta) {
|
|
27
28
|
send(stream, HTTP_STATUS_TOO_MANY_REQUESTS, {
|
|
28
|
-
[HTTP2_HEADER_RETRY_AFTER]: limitInfo.
|
|
29
|
+
[HTTP2_HEADER_RETRY_AFTER]: `${limitInfo.resetSeconds}`,
|
|
29
30
|
[HTTP_HEADER_RATE_LIMIT]: RateLimit.from(limitInfo),
|
|
30
31
|
[HTTP_HEADER_RATE_LIMIT_POLICY]: RateLimitPolicy.from(...policies)
|
|
31
32
|
},
|
|
32
33
|
CONTENT_TYPE_TEXT,
|
|
33
|
-
`Retry After ${limitInfo.
|
|
34
|
+
`Retry After ${limitInfo.resetSeconds} Seconds`,
|
|
34
35
|
meta)
|
|
35
36
|
}
|