@johntalton/http-util 2.0.5 → 3.0.1
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 +101 -24
- 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,50 @@ export function isQuoted(etag) {
|
|
|
40
79
|
return true
|
|
41
80
|
}
|
|
42
81
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
49
91
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|
|
61
106
|
|
|
62
107
|
export class Conditional {
|
|
108
|
+
/**
|
|
109
|
+
* @param {EtagItem|undefined} etagItem
|
|
110
|
+
* @returns {string|undefined}
|
|
111
|
+
*/
|
|
112
|
+
static encodeEtag(etagItem) {
|
|
113
|
+
if(etagItem === undefined) { return undefined }
|
|
114
|
+
if(etagItem.any) {
|
|
115
|
+
if(etagItem.etag !== CONDITION_ETAG_ANY) { return undefined }
|
|
116
|
+
return CONDITION_ETAG_ANY
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if(etagItem.etag === CONDITION_ETAG_ANY) { return undefined }
|
|
120
|
+
if(!isValidEtag(etagItem.etag)) { return undefined }
|
|
121
|
+
|
|
122
|
+
const prefix = etagItem.weak ? CONDITION_ETAG_WEAK_PREFIX : ''
|
|
123
|
+
return `${prefix}${ETAG_QUOTE}${etagItem.etag}${ETAG_QUOTE}`
|
|
124
|
+
}
|
|
125
|
+
|
|
63
126
|
/**
|
|
64
127
|
* @param {string|undefined} matchHeader
|
|
65
128
|
* @returns {Array<EtagItem>}
|
|
@@ -85,12 +148,7 @@ export class Conditional {
|
|
|
85
148
|
}
|
|
86
149
|
})
|
|
87
150
|
.map(item => {
|
|
88
|
-
if(item.etag === CONDITION_ETAG_ANY) {
|
|
89
|
-
return {
|
|
90
|
-
...item,
|
|
91
|
-
any: true
|
|
92
|
-
}
|
|
93
|
-
}
|
|
151
|
+
if(item.etag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
|
|
94
152
|
|
|
95
153
|
// validated quoted
|
|
96
154
|
if(!isQuoted(item.etag)) { return undefined }
|
|
@@ -98,11 +156,14 @@ export class Conditional {
|
|
|
98
156
|
if(!isValidEtag(etag)) { return undefined }
|
|
99
157
|
if(etag === CONDITION_ETAG_ANY) { return undefined }
|
|
100
158
|
|
|
101
|
-
|
|
159
|
+
/** @type {WeakEtagItem | NotWeakEtagItem} */
|
|
160
|
+
const result = {
|
|
102
161
|
weak: item.weak,
|
|
103
162
|
any: false,
|
|
104
163
|
etag
|
|
105
164
|
}
|
|
165
|
+
|
|
166
|
+
return result
|
|
106
167
|
})
|
|
107
168
|
.filter(item => item !== undefined)
|
|
108
169
|
}
|
|
@@ -187,6 +248,20 @@ export class Conditional {
|
|
|
187
248
|
}
|
|
188
249
|
}
|
|
189
250
|
|
|
251
|
+
// Ok
|
|
252
|
+
// console.log(Conditional.encodeEtag({ any: true, weak: false, etag: '*' }))
|
|
253
|
+
// console.log(Conditional.encodeEtag({ any: true, weak: true, etag: '*' }))
|
|
254
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo' }))
|
|
255
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: true, etag: 'WeakFoo' }))
|
|
256
|
+
|
|
257
|
+
// Error
|
|
258
|
+
// console.log(Conditional.encodeEtag(undefined))
|
|
259
|
+
// console.log(Conditional.encodeEtag({ any: true, weak: false, etag: 'NotAsterisk' }))
|
|
260
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo\tBar' }))
|
|
261
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo"Bar' }))
|
|
262
|
+
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: '*' }))
|
|
263
|
+
|
|
264
|
+
|
|
190
265
|
// Ok
|
|
191
266
|
// console.log(Conditional.parseEtagList('"bfc13a64729c4290ef5b2c2730249c88ca92d82d"'))
|
|
192
267
|
// console.log(Conditional.parseEtagList('W/"67ab43", "54ed21", "7892dd"'))
|
|
@@ -225,6 +300,8 @@ export class Conditional {
|
|
|
225
300
|
// }
|
|
226
301
|
// }
|
|
227
302
|
|
|
303
|
+
|
|
304
|
+
|
|
228
305
|
// const testBad = [
|
|
229
306
|
// undefined,
|
|
230
307
|
// 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
|
}
|