@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "2.0.5",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
- * @typedef {Object} EtagItem
45
- * @property {boolean} weak
46
- * @property {boolean} any
47
- * @property {string} etag
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
- * @typedef {Object} IMFFixDate
52
- * @property {typeof DATE_DAYS[number]} dayName
53
- * @property {number} day
54
- * @property {typeof DATE_MONTHS[number]} month
55
- * @property {number} year
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
- return {
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,
@@ -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
  }
@@ -23,7 +23,6 @@ export const PREFLIGHT_AGE_SECONDS = '500'
23
23
  * @property {Array<TimingsInfo>} performance
24
24
  * @property {string|undefined} servername
25
25
  * @property {string|undefined} origin
26
- * @property {string|undefined} [etag]
27
26
  */
28
27
 
29
28
  /**
@@ -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]: `"${meta.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]: `"${meta.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 {number} age
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]: `"${meta.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 {*} limitInfo
23
- * @param {Array<any>} policies
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.retryAfterS,
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.retryAfterS} Seconds`,
34
+ `Retry After ${limitInfo.resetSeconds} Seconds`,
34
35
  meta)
35
36
  }