undici 7.1.0 → 7.2.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/README.md +25 -9
- package/docs/docs/api/ProxyAgent.md +10 -2
- package/index.js +2 -9
- package/lib/cache/sqlite-cache-store.js +5 -1
- package/lib/dispatcher/client-h2.js +9 -0
- package/lib/handler/cache-handler.js +165 -110
- package/lib/handler/wrap-handler.js +3 -6
- package/lib/interceptor/cache.js +19 -7
- package/lib/interceptor/dns.js +6 -2
- package/lib/interceptor/response-error.js +3 -3
- package/lib/util/cache.js +0 -1
- package/lib/util/date.js +259 -0
- package/lib/web/fetch/data-url.js +2 -2
- package/package.json +5 -4
- package/types/index.d.ts +2 -1
package/README.md
CHANGED
|
@@ -281,17 +281,23 @@ stalls or deadlocks when running out of connections.
|
|
|
281
281
|
|
|
282
282
|
```js
|
|
283
283
|
// Do
|
|
284
|
-
const headers = await fetch(url)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
return res.headers
|
|
290
|
-
})
|
|
284
|
+
const { body, headers } = await fetch(url);
|
|
285
|
+
for await (const chunk of body) {
|
|
286
|
+
// force consumption of body
|
|
287
|
+
}
|
|
291
288
|
|
|
292
289
|
// Do not
|
|
293
|
-
const headers = await fetch(url)
|
|
294
|
-
|
|
290
|
+
const { headers } = await fetch(url);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The same applies for `request` too:
|
|
294
|
+
```js
|
|
295
|
+
// Do
|
|
296
|
+
const { body, headers } = await request(url);
|
|
297
|
+
await res.body.dump(); // force consumption of body
|
|
298
|
+
|
|
299
|
+
// Do not
|
|
300
|
+
const { headers } = await request(url);
|
|
295
301
|
```
|
|
296
302
|
|
|
297
303
|
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
|
|
@@ -445,6 +451,16 @@ and `undici.Agent`) which will enable the family autoselection algorithm when es
|
|
|
445
451
|
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
|
|
446
452
|
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
|
|
447
453
|
|
|
454
|
+
## Long Term Support
|
|
455
|
+
|
|
456
|
+
Undici aligns with the Node.js LTS schedule. The following table shows the supported versions:
|
|
457
|
+
|
|
458
|
+
| Version | Node.js | End of Life |
|
|
459
|
+
|---------|-------------|-------------|
|
|
460
|
+
| 5.x | v18.x | 2024-04-30 |
|
|
461
|
+
| 6.x | v20.x v22.x | 2026-04-30 |
|
|
462
|
+
| 7.x | v24.x | 2027-04-30 |
|
|
463
|
+
|
|
448
464
|
## License
|
|
449
465
|
|
|
450
466
|
MIT
|
|
@@ -15,6 +15,7 @@ Returns: `ProxyAgent`
|
|
|
15
15
|
### Parameter: `ProxyAgentOptions`
|
|
16
16
|
|
|
17
17
|
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
|
|
18
|
+
> It ommits `AgentOptions#connect`.
|
|
18
19
|
|
|
19
20
|
* **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string.
|
|
20
21
|
If the `uri` is provided as a string or `uri` is an object with an `uri` property of type string, then it will be parsed into a `URL` object according to the [WHATWG URL Specification](https://url.spec.whatwg.org).
|
|
@@ -22,8 +23,8 @@ For detailed information on the parsing process and potential validation errors,
|
|
|
22
23
|
* **token** `string` (optional) - It can be passed by a string of token for authentication.
|
|
23
24
|
* **auth** `string` (**deprecated**) - Use token.
|
|
24
25
|
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
|
|
25
|
-
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request.
|
|
26
|
-
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server.
|
|
26
|
+
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
|
27
|
+
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
|
27
28
|
|
|
28
29
|
Examples:
|
|
29
30
|
|
|
@@ -35,6 +36,13 @@ const proxyAgent = new ProxyAgent('my.proxy.server')
|
|
|
35
36
|
const proxyAgent = new ProxyAgent(new URL('my.proxy.server'))
|
|
36
37
|
// or
|
|
37
38
|
const proxyAgent = new ProxyAgent({ uri: 'my.proxy.server' })
|
|
39
|
+
// or
|
|
40
|
+
const proxyAgent = new ProxyAgent({
|
|
41
|
+
uri: new URL('my.proxy.server'),
|
|
42
|
+
proxyTls: {
|
|
43
|
+
signal: AbortSignal.timeout(1000)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
38
46
|
```
|
|
39
47
|
|
|
40
48
|
#### Example - Basic ProxyAgent instantiation
|
package/index.js
CHANGED
|
@@ -49,15 +49,8 @@ module.exports.cacheStores = {
|
|
|
49
49
|
MemoryCacheStore: require('./lib/cache/memory-cache-store')
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore
|
|
55
|
-
} catch (err) {
|
|
56
|
-
// Most likely node:sqlite was not present, since SqliteCacheStore is
|
|
57
|
-
// optional, don't throw. Don't check specific error codes here because while
|
|
58
|
-
// ERR_UNKNOWN_BUILTIN_MODULE is expected, users have seen other codes like
|
|
59
|
-
// MODULE_NOT_FOUND
|
|
60
|
-
}
|
|
52
|
+
const SqliteCacheStore = require('./lib/cache/sqlite-cache-store')
|
|
53
|
+
module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore
|
|
61
54
|
|
|
62
55
|
module.exports.buildConnector = buildConnector
|
|
63
56
|
module.exports.errors = errors
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { DatabaseSync } = require('node:sqlite')
|
|
4
3
|
const { Writable } = require('stream')
|
|
5
4
|
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
|
|
6
5
|
|
|
6
|
+
let DatabaseSync
|
|
7
|
+
|
|
7
8
|
const VERSION = 3
|
|
8
9
|
|
|
9
10
|
// 2gb
|
|
@@ -101,6 +102,9 @@ module.exports = class SqliteCacheStore {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
if (!DatabaseSync) {
|
|
106
|
+
DatabaseSync = require('node:sqlite').DatabaseSync
|
|
107
|
+
}
|
|
104
108
|
this.#db = new DatabaseSync(opts?.location ?? ':memory:')
|
|
105
109
|
|
|
106
110
|
this.#db.exec(`
|
|
@@ -30,6 +30,7 @@ const {
|
|
|
30
30
|
kClosed,
|
|
31
31
|
kBodyTimeout
|
|
32
32
|
} = require('../core/symbols.js')
|
|
33
|
+
const { channels } = require('../core/diagnostics.js')
|
|
33
34
|
|
|
34
35
|
const kOpenStreams = Symbol('open streams')
|
|
35
36
|
|
|
@@ -448,6 +449,14 @@ function writeH2 (client, request) {
|
|
|
448
449
|
|
|
449
450
|
session.ref()
|
|
450
451
|
|
|
452
|
+
if (channels.sendHeaders.hasSubscribers) {
|
|
453
|
+
let header = ''
|
|
454
|
+
for (const key in headers) {
|
|
455
|
+
header += `${key}: ${headers[key]}\r\n`
|
|
456
|
+
}
|
|
457
|
+
channels.sendHeaders.publish({ request, headers: header, socket: session[kSocket] })
|
|
458
|
+
}
|
|
459
|
+
|
|
451
460
|
// TODO(metcoder95): add support for sending trailers
|
|
452
461
|
const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
|
|
453
462
|
if (expectContinue) {
|
|
@@ -6,9 +6,17 @@ const {
|
|
|
6
6
|
parseVaryHeader,
|
|
7
7
|
isEtagUsable
|
|
8
8
|
} = require('../util/cache')
|
|
9
|
+
const { parseHttpDate } = require('../util/date.js')
|
|
9
10
|
|
|
10
11
|
function noop () {}
|
|
11
12
|
|
|
13
|
+
// Status codes that we can use some heuristics on to cache
|
|
14
|
+
const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
|
|
15
|
+
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const MAX_RESPONSE_AGE = 2147483647000
|
|
19
|
+
|
|
12
20
|
/**
|
|
13
21
|
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
|
|
14
22
|
*
|
|
@@ -68,17 +76,23 @@ class CacheHandler {
|
|
|
68
76
|
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
|
|
81
|
+
* @param {number} statusCode
|
|
82
|
+
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
83
|
+
* @param {string} statusMessage
|
|
84
|
+
*/
|
|
71
85
|
onResponseStart (
|
|
72
86
|
controller,
|
|
73
87
|
statusCode,
|
|
74
|
-
|
|
88
|
+
resHeaders,
|
|
75
89
|
statusMessage
|
|
76
90
|
) {
|
|
77
91
|
const downstreamOnHeaders = () =>
|
|
78
92
|
this.#handler.onResponseStart?.(
|
|
79
93
|
controller,
|
|
80
94
|
statusCode,
|
|
81
|
-
|
|
95
|
+
resHeaders,
|
|
82
96
|
statusMessage
|
|
83
97
|
)
|
|
84
98
|
|
|
@@ -87,97 +101,113 @@ class CacheHandler {
|
|
|
87
101
|
statusCode >= 200 &&
|
|
88
102
|
statusCode <= 399
|
|
89
103
|
) {
|
|
90
|
-
//
|
|
104
|
+
// Successful response to an unsafe method, delete it from cache
|
|
105
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
|
|
91
106
|
try {
|
|
92
|
-
this.#store.delete(this.#cacheKey)
|
|
107
|
+
this.#store.delete(this.#cacheKey)?.catch?.(noop)
|
|
93
108
|
} catch {
|
|
94
109
|
// Fail silently
|
|
95
110
|
}
|
|
96
111
|
return downstreamOnHeaders()
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
const cacheControlHeader =
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
const cacheControlHeader = resHeaders['cache-control']
|
|
115
|
+
const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)
|
|
116
|
+
if (
|
|
117
|
+
!cacheControlHeader &&
|
|
118
|
+
!resHeaders['expires'] &&
|
|
119
|
+
!heuristicallyCacheable &&
|
|
120
|
+
!this.#cacheByDefault
|
|
121
|
+
) {
|
|
122
|
+
// Don't have anything to tell us this response is cachable and we're not
|
|
123
|
+
// caching by default
|
|
102
124
|
return downstreamOnHeaders()
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
106
|
-
if (!canCacheResponse(this.#cacheType, statusCode,
|
|
128
|
+
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) {
|
|
107
129
|
return downstreamOnHeaders()
|
|
108
130
|
}
|
|
109
131
|
|
|
110
|
-
const age = getAge(headers)
|
|
111
|
-
|
|
112
132
|
const now = Date.now()
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
const resAge = resHeaders.age ? getAge(resHeaders.age) : undefined
|
|
134
|
+
if (resAge && resAge >= MAX_RESPONSE_AGE) {
|
|
135
|
+
// Response considered stale
|
|
136
|
+
return downstreamOnHeaders()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resDate = typeof resHeaders.date === 'string'
|
|
140
|
+
? parseHttpDate(resHeaders.date)
|
|
141
|
+
: undefined
|
|
142
|
+
|
|
143
|
+
const staleAt =
|
|
144
|
+
determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
|
|
145
|
+
this.#cacheByDefault
|
|
146
|
+
if (staleAt === undefined || (resAge && resAge > staleAt)) {
|
|
147
|
+
return downstreamOnHeaders()
|
|
148
|
+
}
|
|
123
149
|
|
|
124
|
-
|
|
150
|
+
const baseTime = resDate ? resDate.getTime() : now
|
|
151
|
+
const absoluteStaleAt = staleAt + baseTime
|
|
152
|
+
if (now >= absoluteStaleAt) {
|
|
153
|
+
// Response is already stale
|
|
154
|
+
return downstreamOnHeaders()
|
|
155
|
+
}
|
|
125
156
|
|
|
126
|
-
|
|
127
|
-
|
|
157
|
+
let varyDirectives
|
|
158
|
+
if (this.#cacheKey.headers && resHeaders.vary) {
|
|
159
|
+
varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
|
|
160
|
+
if (!varyDirectives) {
|
|
161
|
+
// Parse error
|
|
128
162
|
return downstreamOnHeaders()
|
|
129
163
|
}
|
|
164
|
+
}
|
|
130
165
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
166
|
+
const deleteAt = determineDeleteAt(baseTime, cacheControlDirectives, absoluteStaleAt)
|
|
167
|
+
const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
|
|
171
|
+
*/
|
|
172
|
+
const value = {
|
|
173
|
+
statusCode,
|
|
174
|
+
statusMessage,
|
|
175
|
+
headers: strippedHeaders,
|
|
176
|
+
vary: varyDirectives,
|
|
177
|
+
cacheControlDirectives,
|
|
178
|
+
cachedAt: resAge ? now - resAge : now,
|
|
179
|
+
staleAt: absoluteStaleAt,
|
|
180
|
+
deleteAt
|
|
181
|
+
}
|
|
139
182
|
|
|
140
|
-
|
|
141
|
-
|
|
183
|
+
if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
|
|
184
|
+
value.etag = resHeaders.etag
|
|
185
|
+
}
|
|
142
186
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
statusCode,
|
|
148
|
-
statusMessage,
|
|
149
|
-
headers: strippedHeaders,
|
|
150
|
-
vary: varyDirectives,
|
|
151
|
-
cacheControlDirectives,
|
|
152
|
-
cachedAt: age ? now - (age * 1000) : now,
|
|
153
|
-
staleAt: absoluteStaleAt,
|
|
154
|
-
deleteAt
|
|
155
|
-
}
|
|
187
|
+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
|
|
188
|
+
if (!this.#writeStream) {
|
|
189
|
+
return downstreamOnHeaders()
|
|
190
|
+
}
|
|
156
191
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
192
|
+
const handler = this
|
|
193
|
+
this.#writeStream
|
|
194
|
+
.on('drain', () => controller.resume())
|
|
195
|
+
.on('error', function () {
|
|
196
|
+
// TODO (fix): Make error somehow observable?
|
|
197
|
+
handler.#writeStream = undefined
|
|
198
|
+
|
|
199
|
+
// Delete the value in case the cache store is holding onto state from
|
|
200
|
+
// the call to createWriteStream
|
|
201
|
+
handler.#store.delete(handler.#cacheKey)
|
|
202
|
+
})
|
|
203
|
+
.on('close', function () {
|
|
204
|
+
if (handler.#writeStream === this) {
|
|
205
|
+
handler.#writeStream = undefined
|
|
206
|
+
}
|
|
160
207
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const handler = this
|
|
165
|
-
this.#writeStream
|
|
166
|
-
.on('drain', () => controller.resume())
|
|
167
|
-
.on('error', function () {
|
|
168
|
-
// TODO (fix): Make error somehow observable?
|
|
169
|
-
handler.#writeStream = undefined
|
|
170
|
-
})
|
|
171
|
-
.on('close', function () {
|
|
172
|
-
if (handler.#writeStream === this) {
|
|
173
|
-
handler.#writeStream = undefined
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// TODO (fix): Should we resume even if was paused downstream?
|
|
177
|
-
controller.resume()
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
}
|
|
208
|
+
// TODO (fix): Should we resume even if was paused downstream?
|
|
209
|
+
controller.resume()
|
|
210
|
+
})
|
|
181
211
|
|
|
182
212
|
return downstreamOnHeaders()
|
|
183
213
|
}
|
|
@@ -207,18 +237,15 @@ class CacheHandler {
|
|
|
207
237
|
*
|
|
208
238
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
|
|
209
239
|
* @param {number} statusCode
|
|
210
|
-
* @param {
|
|
240
|
+
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
211
241
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
212
242
|
*/
|
|
213
|
-
function canCacheResponse (cacheType, statusCode,
|
|
243
|
+
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
|
|
214
244
|
if (statusCode !== 200 && statusCode !== 307) {
|
|
215
245
|
return false
|
|
216
246
|
}
|
|
217
247
|
|
|
218
|
-
if (
|
|
219
|
-
cacheControlDirectives['no-cache'] === true ||
|
|
220
|
-
cacheControlDirectives['no-store']
|
|
221
|
-
) {
|
|
248
|
+
if (cacheControlDirectives['no-store']) {
|
|
222
249
|
return false
|
|
223
250
|
}
|
|
224
251
|
|
|
@@ -227,13 +254,13 @@ function canCacheResponse (cacheType, statusCode, headers, cacheControlDirective
|
|
|
227
254
|
}
|
|
228
255
|
|
|
229
256
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
|
|
230
|
-
if (
|
|
257
|
+
if (resHeaders.vary?.includes('*')) {
|
|
231
258
|
return false
|
|
232
259
|
}
|
|
233
260
|
|
|
234
261
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
235
|
-
if (
|
|
236
|
-
if (!cacheControlDirectives.public || typeof
|
|
262
|
+
if (resHeaders.authorization) {
|
|
263
|
+
if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') {
|
|
237
264
|
return false
|
|
238
265
|
}
|
|
239
266
|
|
|
@@ -256,58 +283,77 @@ function canCacheResponse (cacheType, statusCode, headers, cacheControlDirective
|
|
|
256
283
|
}
|
|
257
284
|
|
|
258
285
|
/**
|
|
259
|
-
* @param {
|
|
286
|
+
* @param {string | string[]} ageHeader
|
|
260
287
|
* @returns {number | undefined}
|
|
261
288
|
*/
|
|
262
|
-
function getAge (
|
|
263
|
-
|
|
264
|
-
return undefined
|
|
265
|
-
}
|
|
289
|
+
function getAge (ageHeader) {
|
|
290
|
+
const age = parseInt(Array.isArray(ageHeader) ? ageHeader[0] : ageHeader)
|
|
266
291
|
|
|
267
|
-
|
|
268
|
-
if (isNaN(age) || age >= 2147483647) {
|
|
269
|
-
return undefined
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return age
|
|
292
|
+
return isNaN(age) ? undefined : age * 1000
|
|
273
293
|
}
|
|
274
294
|
|
|
275
295
|
/**
|
|
276
296
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
|
|
277
297
|
* @param {number} now
|
|
278
|
-
* @param {
|
|
298
|
+
* @param {number | undefined} age
|
|
299
|
+
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
300
|
+
* @param {Date | undefined} responseDate
|
|
279
301
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
280
302
|
*
|
|
281
|
-
* @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
|
|
303
|
+
* @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached
|
|
282
304
|
*/
|
|
283
|
-
function determineStaleAt (cacheType, now,
|
|
305
|
+
function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) {
|
|
284
306
|
if (cacheType === 'shared') {
|
|
285
307
|
// Prioritize s-maxage since we're a shared cache
|
|
286
308
|
// s-maxage > max-age > Expire
|
|
287
309
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
|
|
288
310
|
const sMaxAge = cacheControlDirectives['s-maxage']
|
|
289
|
-
if (sMaxAge) {
|
|
290
|
-
return sMaxAge * 1000
|
|
311
|
+
if (sMaxAge !== undefined) {
|
|
312
|
+
return sMaxAge > 0 ? sMaxAge * 1000 : undefined
|
|
291
313
|
}
|
|
292
314
|
}
|
|
293
315
|
|
|
294
316
|
const maxAge = cacheControlDirectives['max-age']
|
|
295
|
-
if (maxAge) {
|
|
296
|
-
return maxAge * 1000
|
|
317
|
+
if (maxAge !== undefined) {
|
|
318
|
+
return maxAge > 0 ? maxAge * 1000 : undefined
|
|
297
319
|
}
|
|
298
320
|
|
|
299
|
-
if (
|
|
321
|
+
if (typeof resHeaders.expires === 'string') {
|
|
300
322
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
|
|
301
|
-
const expiresDate =
|
|
302
|
-
if (expiresDate
|
|
323
|
+
const expiresDate = parseHttpDate(resHeaders.expires)
|
|
324
|
+
if (expiresDate) {
|
|
303
325
|
if (now >= expiresDate.getTime()) {
|
|
304
326
|
return undefined
|
|
305
327
|
}
|
|
306
328
|
|
|
329
|
+
if (responseDate) {
|
|
330
|
+
if (responseDate >= expiresDate) {
|
|
331
|
+
return undefined
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (age !== undefined && age > (expiresDate - responseDate)) {
|
|
335
|
+
return undefined
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
307
339
|
return expiresDate.getTime() - now
|
|
308
340
|
}
|
|
309
341
|
}
|
|
310
342
|
|
|
343
|
+
if (typeof resHeaders['last-modified'] === 'string') {
|
|
344
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
|
|
345
|
+
const lastModified = new Date(resHeaders['last-modified'])
|
|
346
|
+
if (isValidDate(lastModified)) {
|
|
347
|
+
if (lastModified.getTime() >= now) {
|
|
348
|
+
return undefined
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const responseAge = now - lastModified.getTime()
|
|
352
|
+
|
|
353
|
+
return responseAge * 0.1
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
311
357
|
if (cacheControlDirectives.immutable) {
|
|
312
358
|
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
|
|
313
359
|
return 31536000
|
|
@@ -317,10 +363,11 @@ function determineStaleAt (cacheType, now, headers, cacheControlDirectives) {
|
|
|
317
363
|
}
|
|
318
364
|
|
|
319
365
|
/**
|
|
366
|
+
* @param {number} now
|
|
320
367
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
321
368
|
* @param {number} staleAt
|
|
322
369
|
*/
|
|
323
|
-
function determineDeleteAt (cacheControlDirectives, staleAt) {
|
|
370
|
+
function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
324
371
|
let staleWhileRevalidate = -Infinity
|
|
325
372
|
let staleIfError = -Infinity
|
|
326
373
|
let immutable = -Infinity
|
|
@@ -334,7 +381,7 @@ function determineDeleteAt (cacheControlDirectives, staleAt) {
|
|
|
334
381
|
}
|
|
335
382
|
|
|
336
383
|
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
|
|
337
|
-
immutable =
|
|
384
|
+
immutable = now + 31536000000
|
|
338
385
|
}
|
|
339
386
|
|
|
340
387
|
return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
|
|
@@ -342,11 +389,11 @@ function determineDeleteAt (cacheControlDirectives, staleAt) {
|
|
|
342
389
|
|
|
343
390
|
/**
|
|
344
391
|
* Strips headers required to be removed in cached responses
|
|
345
|
-
* @param {
|
|
392
|
+
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
346
393
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
347
394
|
* @returns {Record<string, string | string []>}
|
|
348
395
|
*/
|
|
349
|
-
function stripNecessaryHeaders (
|
|
396
|
+
function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
|
|
350
397
|
const headersToRemove = [
|
|
351
398
|
'connection',
|
|
352
399
|
'proxy-authenticate',
|
|
@@ -360,14 +407,14 @@ function stripNecessaryHeaders (headers, cacheControlDirectives) {
|
|
|
360
407
|
'age'
|
|
361
408
|
]
|
|
362
409
|
|
|
363
|
-
if (
|
|
364
|
-
if (Array.isArray(
|
|
410
|
+
if (resHeaders['connection']) {
|
|
411
|
+
if (Array.isArray(resHeaders['connection'])) {
|
|
365
412
|
// connection: a
|
|
366
413
|
// connection: b
|
|
367
|
-
headersToRemove.push(...
|
|
414
|
+
headersToRemove.push(...resHeaders['connection'].map(header => header.trim()))
|
|
368
415
|
} else {
|
|
369
416
|
// connection: a, b
|
|
370
|
-
headersToRemove.push(...
|
|
417
|
+
headersToRemove.push(...resHeaders['connection'].split(',').map(header => header.trim()))
|
|
371
418
|
}
|
|
372
419
|
}
|
|
373
420
|
|
|
@@ -381,13 +428,21 @@ function stripNecessaryHeaders (headers, cacheControlDirectives) {
|
|
|
381
428
|
|
|
382
429
|
let strippedHeaders
|
|
383
430
|
for (const headerName of headersToRemove) {
|
|
384
|
-
if (
|
|
385
|
-
strippedHeaders ??= { ...
|
|
431
|
+
if (resHeaders[headerName]) {
|
|
432
|
+
strippedHeaders ??= { ...resHeaders }
|
|
386
433
|
delete strippedHeaders[headerName]
|
|
387
434
|
}
|
|
388
435
|
}
|
|
389
436
|
|
|
390
|
-
return strippedHeaders ??
|
|
437
|
+
return strippedHeaders ?? resHeaders
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {Date} date
|
|
442
|
+
* @returns {boolean}
|
|
443
|
+
*/
|
|
444
|
+
function isValidDate (date) {
|
|
445
|
+
return date instanceof Date && Number.isFinite(date.valueOf())
|
|
391
446
|
}
|
|
392
447
|
|
|
393
448
|
module.exports = CacheHandler
|
|
@@ -53,8 +53,7 @@ module.exports = class WrapHandler {
|
|
|
53
53
|
onRequestUpgrade (controller, statusCode, headers, socket) {
|
|
54
54
|
const rawHeaders = []
|
|
55
55
|
for (const [key, val] of Object.entries(headers)) {
|
|
56
|
-
|
|
57
|
-
rawHeaders.push(Buffer.from(key), Buffer.from(val))
|
|
56
|
+
rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
|
|
@@ -63,8 +62,7 @@ module.exports = class WrapHandler {
|
|
|
63
62
|
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
64
63
|
const rawHeaders = []
|
|
65
64
|
for (const [key, val] of Object.entries(headers)) {
|
|
66
|
-
|
|
67
|
-
rawHeaders.push(Buffer.from(key), Buffer.from(val))
|
|
65
|
+
rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) {
|
|
@@ -81,8 +79,7 @@ module.exports = class WrapHandler {
|
|
|
81
79
|
onResponseEnd (controller, trailers) {
|
|
82
80
|
const rawTrailers = []
|
|
83
81
|
for (const [key, val] of Object.entries(trailers)) {
|
|
84
|
-
|
|
85
|
-
rawTrailers.push(Buffer.from(key), Buffer.from(val))
|
|
82
|
+
rawTrailers.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
this.#handler.onComplete?.(rawTrailers)
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -103,10 +103,14 @@ function handleUncachedResponse (
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
|
|
107
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
|
|
106
108
|
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
107
109
|
* @param {number} age
|
|
110
|
+
* @param {any} context
|
|
111
|
+
* @param {boolean} isStale
|
|
108
112
|
*/
|
|
109
|
-
function sendCachedValue (handler, opts, result, age, context) {
|
|
113
|
+
function sendCachedValue (handler, opts, result, age, context, isStale) {
|
|
110
114
|
// TODO (perf): Readable.from path can be optimized...
|
|
111
115
|
const stream = util.isStream(result.body)
|
|
112
116
|
? result.body
|
|
@@ -160,8 +164,13 @@ function sendCachedValue (handler, opts, result, age, context) {
|
|
|
160
164
|
|
|
161
165
|
// Add the age header
|
|
162
166
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
const headers = { ...result.headers, age: String(age) }
|
|
168
|
+
|
|
169
|
+
if (isStale) {
|
|
170
|
+
// Add warning header
|
|
171
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning
|
|
172
|
+
headers.warning = '110 - "response is stale"'
|
|
173
|
+
}
|
|
165
174
|
|
|
166
175
|
handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
|
|
167
176
|
|
|
@@ -225,8 +234,11 @@ function handleResult (
|
|
|
225
234
|
|
|
226
235
|
let headers = {
|
|
227
236
|
...opts.headers,
|
|
228
|
-
'if-modified-since': new Date(result.cachedAt).toUTCString()
|
|
229
|
-
|
|
237
|
+
'if-modified-since': new Date(result.cachedAt).toUTCString()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (result.etag) {
|
|
241
|
+
headers['if-none-match'] = result.etag
|
|
230
242
|
}
|
|
231
243
|
|
|
232
244
|
if (result.vary) {
|
|
@@ -245,7 +257,7 @@ function handleResult (
|
|
|
245
257
|
new CacheRevalidationHandler(
|
|
246
258
|
(success, context) => {
|
|
247
259
|
if (success) {
|
|
248
|
-
sendCachedValue(handler, opts, result, age, context)
|
|
260
|
+
sendCachedValue(handler, opts, result, age, context, true)
|
|
249
261
|
} else if (util.isStream(result.body)) {
|
|
250
262
|
result.body.on('error', () => {}).destroy()
|
|
251
263
|
}
|
|
@@ -261,7 +273,7 @@ function handleResult (
|
|
|
261
273
|
opts.body.on('error', () => {}).destroy()
|
|
262
274
|
}
|
|
263
275
|
|
|
264
|
-
sendCachedValue(handler, opts, result, age, null)
|
|
276
|
+
sendCachedValue(handler, opts, result, age, null, false)
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
/**
|
package/lib/interceptor/dns.js
CHANGED
|
@@ -213,6 +213,10 @@ class DNSInstance {
|
|
|
213
213
|
this.#records.set(origin.hostname, records)
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
deleteRecords (origin) {
|
|
217
|
+
this.#records.delete(origin.hostname)
|
|
218
|
+
}
|
|
219
|
+
|
|
216
220
|
getHandler (meta, opts) {
|
|
217
221
|
return new DNSDispatchHandler(this, meta, opts)
|
|
218
222
|
}
|
|
@@ -261,7 +265,7 @@ class DNSDispatchHandler extends DecoratorHandler {
|
|
|
261
265
|
break
|
|
262
266
|
}
|
|
263
267
|
case 'ENOTFOUND':
|
|
264
|
-
this.#state.
|
|
268
|
+
this.#state.deleteRecords(this.#origin)
|
|
265
269
|
// eslint-disable-next-line no-fallthrough
|
|
266
270
|
default:
|
|
267
271
|
super.onResponseError(controller, err)
|
|
@@ -358,7 +362,7 @@ module.exports = interceptorOpts => {
|
|
|
358
362
|
servername: origin.hostname, // For SNI on TLS
|
|
359
363
|
origin: newOrigin,
|
|
360
364
|
headers: {
|
|
361
|
-
host: origin.
|
|
365
|
+
host: origin.host,
|
|
362
366
|
...origDispatchOpts.headers
|
|
363
367
|
}
|
|
364
368
|
}
|
|
@@ -16,7 +16,7 @@ class ResponseErrorHandler extends DecoratorHandler {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
#checkContentType (contentType) {
|
|
19
|
-
return this.#contentType.indexOf(contentType) === 0
|
|
19
|
+
return (this.#contentType ?? '').indexOf(contentType) === 0
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
onRequestStart (controller, context) {
|
|
@@ -81,8 +81,8 @@ class ResponseErrorHandler extends DecoratorHandler {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
onResponseError (err) {
|
|
85
|
-
super.onResponseError(err)
|
|
84
|
+
onResponseError (controller, err) {
|
|
85
|
+
super.onResponseError(controller, err)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
package/lib/util/cache.js
CHANGED
package/lib/util/date.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const IMF_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
|
4
|
+
const IMF_SPACES = [4, 7, 11, 16, 25]
|
|
5
|
+
const IMF_MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
|
6
|
+
const IMF_COLONS = [19, 22]
|
|
7
|
+
|
|
8
|
+
const ASCTIME_SPACES = [3, 7, 10, 19]
|
|
9
|
+
|
|
10
|
+
const RFC850_DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @see https://www.rfc-editor.org/rfc/rfc9110.html#name-date-time-formats
|
|
14
|
+
*
|
|
15
|
+
* @param {string} date
|
|
16
|
+
* @param {Date} [now]
|
|
17
|
+
* @returns {Date | undefined}
|
|
18
|
+
*/
|
|
19
|
+
function parseHttpDate (date, now) {
|
|
20
|
+
// Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
|
|
21
|
+
// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
|
|
22
|
+
// Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
|
|
23
|
+
|
|
24
|
+
date = date.toLowerCase()
|
|
25
|
+
|
|
26
|
+
switch (date[3]) {
|
|
27
|
+
case ',': return parseImfDate(date)
|
|
28
|
+
case ' ': return parseAscTimeDate(date)
|
|
29
|
+
default: return parseRfc850Date(date, now)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @see https://httpwg.org/specs/rfc9110.html#preferred.date.format
|
|
35
|
+
*
|
|
36
|
+
* @param {string} date
|
|
37
|
+
* @returns {Date | undefined}
|
|
38
|
+
*/
|
|
39
|
+
function parseImfDate (date) {
|
|
40
|
+
if (date.length !== 29) {
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!date.endsWith('gmt')) {
|
|
45
|
+
// Unsupported timezone
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const spaceInx of IMF_SPACES) {
|
|
50
|
+
if (date[spaceInx] !== ' ') {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const colonIdx of IMF_COLONS) {
|
|
56
|
+
if (date[colonIdx] !== ':') {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const dayName = date.substring(0, 3)
|
|
62
|
+
if (!IMF_DAYS.includes(dayName)) {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dayString = date.substring(5, 7)
|
|
67
|
+
const day = Number.parseInt(dayString)
|
|
68
|
+
if (isNaN(day) || (day < 10 && dayString[0] !== '0')) {
|
|
69
|
+
// Not a number, 0, or it's less than 10 and didn't start with a 0
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const month = date.substring(8, 11)
|
|
74
|
+
const monthIdx = IMF_MONTHS.indexOf(month)
|
|
75
|
+
if (monthIdx === -1) {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const year = Number.parseInt(date.substring(12, 16))
|
|
80
|
+
if (isNaN(year)) {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hourString = date.substring(17, 19)
|
|
85
|
+
const hour = Number.parseInt(hourString)
|
|
86
|
+
if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const minuteString = date.substring(20, 22)
|
|
91
|
+
const minute = Number.parseInt(minuteString)
|
|
92
|
+
if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const secondString = date.substring(23, 25)
|
|
97
|
+
const second = Number.parseInt(secondString)
|
|
98
|
+
if (isNaN(second) || (second < 10 && secondString[0] !== '0')) {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
|
|
107
|
+
*
|
|
108
|
+
* @param {string} date
|
|
109
|
+
* @returns {Date | undefined}
|
|
110
|
+
*/
|
|
111
|
+
function parseAscTimeDate (date) {
|
|
112
|
+
// This is assumed to be in UTC
|
|
113
|
+
|
|
114
|
+
if (date.length !== 24) {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const spaceIdx of ASCTIME_SPACES) {
|
|
119
|
+
if (date[spaceIdx] !== ' ') {
|
|
120
|
+
return undefined
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dayName = date.substring(0, 3)
|
|
125
|
+
if (!IMF_DAYS.includes(dayName)) {
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const month = date.substring(4, 7)
|
|
130
|
+
const monthIdx = IMF_MONTHS.indexOf(month)
|
|
131
|
+
if (monthIdx === -1) {
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const dayString = date.substring(8, 10)
|
|
136
|
+
const day = Number.parseInt(dayString)
|
|
137
|
+
if (isNaN(day) || (day < 10 && dayString[0] !== ' ')) {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const hourString = date.substring(11, 13)
|
|
142
|
+
const hour = Number.parseInt(hourString)
|
|
143
|
+
if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) {
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const minuteString = date.substring(14, 16)
|
|
148
|
+
const minute = Number.parseInt(minuteString)
|
|
149
|
+
if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) {
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const secondString = date.substring(17, 19)
|
|
154
|
+
const second = Number.parseInt(secondString)
|
|
155
|
+
if (isNaN(second) || (second < 10 && secondString[0] !== '0')) {
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const year = Number.parseInt(date.substring(20, 24))
|
|
160
|
+
if (isNaN(year)) {
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
|
|
169
|
+
*
|
|
170
|
+
* @param {string} date
|
|
171
|
+
* @param {Date} [now]
|
|
172
|
+
* @returns {Date | undefined}
|
|
173
|
+
*/
|
|
174
|
+
function parseRfc850Date (date, now = new Date()) {
|
|
175
|
+
if (!date.endsWith('gmt')) {
|
|
176
|
+
// Unsupported timezone
|
|
177
|
+
return undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const commaIndex = date.indexOf(',')
|
|
181
|
+
if (commaIndex === -1) {
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if ((date.length - commaIndex - 1) !== 23) {
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const dayName = date.substring(0, commaIndex)
|
|
190
|
+
if (!RFC850_DAYS.includes(dayName)) {
|
|
191
|
+
return undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
date[commaIndex + 1] !== ' ' ||
|
|
196
|
+
date[commaIndex + 4] !== '-' ||
|
|
197
|
+
date[commaIndex + 8] !== '-' ||
|
|
198
|
+
date[commaIndex + 11] !== ' ' ||
|
|
199
|
+
date[commaIndex + 14] !== ':' ||
|
|
200
|
+
date[commaIndex + 17] !== ':' ||
|
|
201
|
+
date[commaIndex + 20] !== ' '
|
|
202
|
+
) {
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const dayString = date.substring(commaIndex + 2, commaIndex + 4)
|
|
207
|
+
const day = Number.parseInt(dayString)
|
|
208
|
+
if (isNaN(day) || (day < 10 && dayString[0] !== '0')) {
|
|
209
|
+
// Not a number, or it's less than 10 and didn't start with a 0
|
|
210
|
+
return undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const month = date.substring(commaIndex + 5, commaIndex + 8)
|
|
214
|
+
const monthIdx = IMF_MONTHS.indexOf(month)
|
|
215
|
+
if (monthIdx === -1) {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// As of this point year is just the decade (i.e. 94)
|
|
220
|
+
let year = Number.parseInt(date.substring(commaIndex + 9, commaIndex + 11))
|
|
221
|
+
if (isNaN(year)) {
|
|
222
|
+
return undefined
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const currentYear = now.getUTCFullYear()
|
|
226
|
+
const currentDecade = currentYear % 100
|
|
227
|
+
const currentCentury = Math.floor(currentYear / 100)
|
|
228
|
+
|
|
229
|
+
if (year > currentDecade && year - currentDecade >= 50) {
|
|
230
|
+
// Over 50 years in future, go to previous century
|
|
231
|
+
year += (currentCentury - 1) * 100
|
|
232
|
+
} else {
|
|
233
|
+
year += currentCentury * 100
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const hourString = date.substring(commaIndex + 12, commaIndex + 14)
|
|
237
|
+
const hour = Number.parseInt(hourString)
|
|
238
|
+
if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) {
|
|
239
|
+
return undefined
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const minuteString = date.substring(commaIndex + 15, commaIndex + 17)
|
|
243
|
+
const minute = Number.parseInt(minuteString)
|
|
244
|
+
if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) {
|
|
245
|
+
return undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const secondString = date.substring(commaIndex + 18, commaIndex + 20)
|
|
249
|
+
const second = Number.parseInt(secondString)
|
|
250
|
+
if (isNaN(second) || (second < 10 && secondString[0] !== '0')) {
|
|
251
|
+
return undefined
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
parseHttpDate
|
|
259
|
+
}
|
|
@@ -283,7 +283,7 @@ function parseMIMEType (input) {
|
|
|
283
283
|
|
|
284
284
|
// 5. If position is past the end of input, then return
|
|
285
285
|
// failure
|
|
286
|
-
if (position.position
|
|
286
|
+
if (position.position >= input.length) {
|
|
287
287
|
return 'failure'
|
|
288
288
|
}
|
|
289
289
|
|
|
@@ -364,7 +364,7 @@ function parseMIMEType (input) {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
// 6. If position is past the end of input, then break.
|
|
367
|
-
if (position.position
|
|
367
|
+
if (position.position >= input.length) {
|
|
368
368
|
break
|
|
369
369
|
}
|
|
370
370
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"lint:fix": "eslint --fix --cache",
|
|
70
70
|
"test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
|
|
71
71
|
"test:javascript": "npm run test:javascript:no-jest && npm run test:jest",
|
|
72
|
-
"test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test",
|
|
72
|
+
"test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests",
|
|
73
73
|
"test:javascript:without-intl": "npm run test:javascript:no-jest",
|
|
74
74
|
"test:busboy": "borp -p \"test/busboy/*.js\"",
|
|
75
75
|
"test:cache": "borp -p \"test/cache/*.js\"",
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"test:websocket:autobahn:report": "node test/autobahn/report.js",
|
|
97
97
|
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
|
|
98
98
|
"test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
|
|
99
|
+
"test:cache-tests": "node test/cache-interceptor/cache-tests.mjs --ci",
|
|
99
100
|
"coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
|
|
100
101
|
"coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report:ci",
|
|
101
102
|
"coverage:clean": "node ./scripts/clean-coverage.js",
|
|
@@ -106,7 +107,7 @@
|
|
|
106
107
|
"prepare": "husky && node ./scripts/platform-shell.js"
|
|
107
108
|
},
|
|
108
109
|
"devDependencies": {
|
|
109
|
-
"@fastify/busboy": "3.
|
|
110
|
+
"@fastify/busboy": "3.1.0",
|
|
110
111
|
"@matteo.collina/tspl": "^0.1.1",
|
|
111
112
|
"@sinonjs/fake-timers": "^12.0.0",
|
|
112
113
|
"@types/node": "^18.19.50",
|
|
@@ -121,7 +122,7 @@
|
|
|
121
122
|
"https-pem": "^3.0.0",
|
|
122
123
|
"husky": "^9.0.7",
|
|
123
124
|
"jest": "^29.0.2",
|
|
124
|
-
"neostandard": "^0.
|
|
125
|
+
"neostandard": "^0.12.0",
|
|
125
126
|
"node-forge": "^1.3.1",
|
|
126
127
|
"proxy": "^2.1.1",
|
|
127
128
|
"tsd": "^0.31.2",
|
package/types/index.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ declare namespace Undici {
|
|
|
64
64
|
const caches: typeof import('./cache').caches
|
|
65
65
|
const interceptors: typeof import('./interceptors').default
|
|
66
66
|
const cacheStores: {
|
|
67
|
-
MemoryCacheStore: typeof import('./cache-interceptor').default.MemoryCacheStore
|
|
67
|
+
MemoryCacheStore: typeof import('./cache-interceptor').default.MemoryCacheStore,
|
|
68
|
+
SqliteCacheStore: typeof import('./cache-interceptor').default.SqliteCacheStore
|
|
68
69
|
}
|
|
69
70
|
}
|