@nxtedition/nxt-undici 5.1.7 → 5.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.
@@ -1,134 +1,116 @@
1
- import { LRUCache } from 'lru-cache'
2
- import { parseHeaders, parseCacheControl } from '../utils.js'
1
+ import { SqliteCacheStore } from '../cache/sqlite-cache-store.js'
2
+ import { DecoratorHandler, parseHeaders, parseCacheControl } from '../utils.js'
3
3
 
4
- class CacheHandler {
5
- #handler
6
- #store
7
- #key
4
+ const DEFAULT_STORE = new SqliteCacheStore({ location: ':memory:' })
5
+
6
+ class CacheHandler extends DecoratorHandler {
8
7
  #value
8
+ #opts
9
+ #store
9
10
 
10
- constructor({ key, handler, store }) {
11
- this.#key = key
12
- this.#handler = handler
11
+ constructor(opts, { store, handler }) {
12
+ super(handler)
13
+
14
+ this.#opts = opts
13
15
  this.#store = store
14
16
  }
15
17
 
16
18
  onConnect(abort) {
17
19
  this.#value = null
18
20
 
19
- return this.#handler.onConnect(abort)
21
+ super.onConnect(abort)
20
22
  }
21
23
 
22
24
  onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
23
25
  if (statusCode !== 307) {
24
- return this.#handler.onHeaders(statusCode, null, resume, statusMessage, headers)
26
+ // Only cache redirects...
27
+ return super.onHeaders(statusCode, null, resume, null, headers)
25
28
  }
26
29
 
27
- // TODO (fix): Support vary header.
28
- const cacheControl = parseCacheControl(headers['cache-control'])
30
+ if (headers.vary === '*') {
31
+ // Not cacheble...
32
+ return super.onHeaders(statusCode, null, resume, null, headers)
33
+ }
29
34
 
35
+ const cacheControl = parseCacheControl(headers['cache-control'])
30
36
  const contentLength = headers['content-length'] ? Number(headers['content-length']) : Infinity
31
- const maxEntrySize = this.#store.maxEntrySize ?? Infinity
37
+
38
+ if (contentLength) {
39
+ // We don't support caching responses with body...
40
+ return super.onHeaders(statusCode, null, resume, null, headers)
41
+ }
32
42
 
33
43
  if (
34
- (!contentLength || contentLength < maxEntrySize) &&
35
- cacheControl &&
36
- cacheControl.public &&
37
- !cacheControl.private &&
38
- !cacheControl['no-store'] &&
44
+ !cacheControl ||
45
+ !cacheControl.public ||
46
+ cacheControl.private ||
47
+ cacheControl['no-store'] ||
39
48
  // TODO (fix): Support all cache control directives...
40
- // !opts.headers['no-transform'] &&
41
- !cacheControl['no-cache'] &&
42
- !cacheControl['must-understand'] &&
43
- !cacheControl['must-revalidate'] &&
44
- !cacheControl['proxy-revalidate']
49
+ // cacheControl['no-transform'] ||
50
+ cacheControl['no-cache'] ||
51
+ cacheControl['must-understand'] ||
52
+ cacheControl['must-revalidate'] ||
53
+ cacheControl['proxy-revalidate']
45
54
  ) {
46
- const maxAge = cacheControl['s-max-age'] ?? cacheControl['max-age']
47
- const ttl = cacheControl.immutable ? 31556952 : Number(maxAge)
48
-
49
- if (ttl > 0) {
50
- this.#value = {
51
- statusCode,
52
- statusMessage,
53
- headers,
54
- body: [],
55
- size: 256, // TODO (fix): Measure headers size...
56
- ttl: ttl * 1e3,
57
- }
58
- }
55
+ // Not cacheble...
56
+ return super.onHeaders(statusCode, null, resume, null, headers)
59
57
  }
60
58
 
61
- return this.#handler.onHeaders(statusCode, null, resume, statusMessage, headers)
62
- }
63
-
64
- onData(chunk) {
65
- if (this.#value) {
66
- this.#value.size += chunk.bodyLength
67
-
68
- const maxEntrySize = this.#store.maxEntrySize ?? Infinity
69
- if (this.#value.size > maxEntrySize) {
70
- this.#value = null
71
- } else {
72
- this.#value.data.body.push(chunk)
59
+ const vary = {}
60
+ if (headers.vary) {
61
+ for (const key of [headers.vary]
62
+ .flat()
63
+ .flatMap((vary) => vary.split(',').map((key) => key.trim().toLowerCase()))) {
64
+ const val = this.#opts.headers?.[key]
65
+ if (!val) {
66
+ // Expect vary headers to be present...
67
+ return super.onHeaders(statusCode, null, resume, null, headers)
68
+ }
69
+ vary[key] = val
73
70
  }
71
+
72
+ // Unexpected vary header type...
73
+ return super.onHeaders(statusCode, null, resume, null, headers)
74
74
  }
75
- return this.#handler.onData(chunk)
76
- }
77
75
 
78
- onComplete() {
79
- if (this.#value) {
80
- this.#store.set(
81
- this.#key,
82
- {
83
- statusCode: this.#value.statusCode,
84
- statusMessage: this.#value.statusMessage,
85
- headers: this.#value.headers,
86
- body: Buffer.concat(this.#value.body),
87
- },
88
- { ttl: this.#value.ttl, size: this.#value.size },
89
- )
76
+ const ttl = cacheControl.immutable
77
+ ? 31556952
78
+ : Number(cacheControl['s-max-age'] ?? cacheControl['max-age'])
79
+ if (!ttl || !Number.isFinite(ttl) || ttl <= 0) {
80
+ return super.onHeaders(statusCode, null, resume, null, headers)
90
81
  }
91
- return this.#handler.onComplete()
92
- }
93
82
 
94
- onError(err) {
95
- this.#handler.onError(err)
96
- }
97
- }
83
+ const cachedAt = Date.now()
84
+
85
+ this.#value = {
86
+ body: null,
87
+ deleteAt: cachedAt + ttl * 1e3,
88
+ statusCode,
89
+ statusMessage: '',
90
+ headers,
91
+ cacheControlDirectives: '',
92
+ etag: '',
93
+ vary,
94
+ cachedAt,
95
+ staleAt: 0,
96
+ }
98
97
 
99
- class MemoryCacheStore {
100
- constructor({ maxSize = 1024 * 1024, maxEntrySize = 128 * 1024, maxTTL = 48 * 3600e3 }) {
101
- this.maxSize = maxSize
102
- this.maxEntrySize = maxEntrySize
103
- this.maxTTL = maxTTL
104
- this.cache = new LRUCache({ maxSize })
98
+ return super.onHeaders(statusCode, null, resume, null, headers)
105
99
  }
106
100
 
107
- set(key, value, opts) {
108
- this.cache.set(
109
- key,
110
- value,
111
- opts
112
- ? {
113
- ttl: opts.ttl ? Math.min(opts.ttl, this.maxTTL) : undefined,
114
- size: opts.size,
115
- }
116
- : undefined,
117
- )
101
+ onData(chunk) {
102
+ this.#value = null
103
+ return super.onData(chunk)
118
104
  }
119
105
 
120
- get(key) {
121
- return this.cache.get(key)
106
+ onComplete() {
107
+ if (this.#value) {
108
+ this.#store.set(this.#opts, this.#value)
109
+ }
110
+ super.onComplete()
122
111
  }
123
112
  }
124
113
 
125
- function makeKey(opts) {
126
- // NOTE: Ignores headers...
127
- return `${opts.origin}:${opts.method}:${opts.path}`
128
- }
129
-
130
- const DEFAULT_CACHE_STORE = new MemoryCacheStore({ maxSize: 128 * 1024, maxEntrySize: 1024 })
131
-
132
114
  export default () => (dispatch) => (opts, handler) => {
133
115
  if (!opts.cache || opts.upgrade) {
134
116
  return dispatch(opts, handler)
@@ -154,48 +136,47 @@ export default () => (dispatch) => (opts, handler) => {
154
136
  // Dump body...
155
137
  opts.body?.on('error', () => {}).resume()
156
138
 
157
- const store = opts.cache === true ? DEFAULT_CACHE_STORE : opts.cache
158
-
159
- if (!store) {
160
- throw new Error(`Cache store not provided.`)
161
- }
162
-
163
- const key = makeKey(opts)
164
- const entry = store.get(key)
165
-
139
+ const store = opts.store ?? DEFAULT_STORE
140
+ const entry = store.get(opts)
166
141
  if (!entry) {
167
- return dispatch(opts, new CacheHandler({ handler, store, key: makeKey(opts) }))
142
+ return dispatch(opts, new CacheHandler(opts.headers, { store, handler }))
168
143
  }
169
144
 
170
- const { statusCode, statusMessage, headers, body } = entry
171
-
172
145
  let aborted = false
173
- const abort = () => {
174
- aborted = true
146
+ let paused = false
147
+ const abort = (reason) => {
148
+ if (!aborted) {
149
+ aborted = true
150
+ handler.onError(reason)
151
+ }
152
+ }
153
+ const resume = () => {
154
+ if (paused && !aborted) {
155
+ handler.onComplete()
156
+ paused = false
157
+ }
175
158
  }
176
- const resume = () => {}
177
159
 
160
+ const { statusCode, headers } = entry
178
161
  try {
179
162
  handler.onConnect(abort)
180
163
  if (aborted) {
181
164
  return true
182
165
  }
183
166
 
184
- handler.onHeaders(statusCode, null, resume, statusMessage, headers)
167
+ if (handler.onHeaders(statusCode, null, resume, null, headers) === false) {
168
+ paused = true
169
+ }
170
+
185
171
  if (aborted) {
186
172
  return true
187
173
  }
188
174
 
189
- if (body.byteKength > 0) {
190
- handler.onData(body)
191
- if (aborted) {
192
- return true
193
- }
175
+ if (!paused) {
176
+ handler.onComplete()
194
177
  }
195
-
196
- handler.onComplete()
197
178
  } catch (err) {
198
- handler.onError(err)
179
+ abort(err)
199
180
  }
200
181
 
201
182
  return true
@@ -1,9 +1,12 @@
1
1
  import net from 'node:net'
2
2
  import { resolve4 } from 'node:dns/promises'
3
+ import { getFastNow } from '../utils.js'
3
4
 
4
5
  export default () => (dispatch) => {
6
+ const cache = new Map()
7
+
5
8
  return async (opts, handler) => {
6
- if (!opts.dns) {
9
+ if (!opts || !opts.dns || !opts.origin) {
7
10
  return dispatch(opts, handler)
8
11
  }
9
12
 
@@ -13,9 +16,32 @@ export default () => (dispatch) => {
13
16
  return dispatch(opts, handler)
14
17
  }
15
18
 
16
- const host = origin.host
17
- const records = await resolve4(origin.hostname)
18
- origin.hostname = records[Math.floor(Math.random() * records.length)]
19
+ const now = getFastNow()
20
+ const { host, hostname } = origin
21
+
22
+ const promiseOrRecords = cache.get(hostname)
23
+
24
+ let records = promiseOrRecords?.then ? await promiseOrRecords : promiseOrRecords
25
+
26
+ records = records.filter(({ expires }) => expires > now)
27
+ if (records == null || records.length === 0) {
28
+ const promise = resolve4(hostname, { ttl: true }).then((records) =>
29
+ records.map(({ address, ttl }) => ({ address, expires: now + 1e3 * ttl })),
30
+ )
31
+ cache.set(hostname, promise)
32
+ records = await promise
33
+ cache.set(hostname, records)
34
+ }
35
+
36
+ if (records == null || records.length === 0) {
37
+ throw Object.assign(new Error('No DNS records found for the specified hostname.'), {
38
+ code: 'ENOTFOUND',
39
+ hostname: origin.hostname,
40
+ })
41
+ }
42
+
43
+ const addresses = records.map(({ address }) => address)
44
+ origin.hostname = addresses[Math.floor(Math.random() * addresses.length)]
19
45
 
20
46
  return dispatch({ ...opts, origin, headers: { ...opts.headers, host } }, handler)
21
47
  }
@@ -0,0 +1,9 @@
1
+ import { test } from 'tap'
2
+ import { request } from '../index.js'
3
+
4
+ test('retry destroy pre response', async (t) => {
5
+ const { body, statusCode } = await request(`http://google.com`)
6
+ await body.dump()
7
+ t.equal(statusCode, 200)
8
+ t.end()
9
+ })
@@ -1,7 +1,6 @@
1
1
  import { DecoratorHandler, parseHeaders } from '../utils.js'
2
2
 
3
3
  class Handler extends DecoratorHandler {
4
- #handler
5
4
  #opts
6
5
  #logger
7
6
 
@@ -23,7 +22,6 @@ class Handler extends DecoratorHandler {
23
22
  constructor(opts, { handler }) {
24
23
  super(handler)
25
24
 
26
- this.#handler = handler
27
25
  this.#opts = opts
28
26
  this.#logger = opts.logger.child({ ureq: opts })
29
27
 
@@ -41,7 +39,7 @@ class Handler extends DecoratorHandler {
41
39
 
42
40
  this.#logger.debug('upstream request started')
43
41
 
44
- return this.#handler.onConnect((reason) => {
42
+ super.onConnect((reason) => {
45
43
  this.#aborted = true
46
44
  this.#abort(reason)
47
45
  })
@@ -62,7 +60,7 @@ class Handler extends DecoratorHandler {
62
60
  this.#logger.debug('upstream request socket closed')
63
61
  })
64
62
 
65
- return this.#handler.onUpgrade(statusCode, null, socket, headers)
63
+ super.onUpgrade(statusCode, null, socket, headers)
66
64
  }
67
65
 
68
66
  onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
@@ -71,7 +69,7 @@ class Handler extends DecoratorHandler {
71
69
  this.#statusCode = statusCode
72
70
  this.#headers = headers
73
71
 
74
- return this.#handler.onHeaders(statusCode, null, resume, statusMessage, headers)
72
+ return super.onHeaders(statusCode, null, resume, null, headers)
75
73
  }
76
74
 
77
75
  onData(chunk) {
@@ -81,7 +79,7 @@ class Handler extends DecoratorHandler {
81
79
 
82
80
  this.#pos += chunk.length
83
81
 
84
- return this.#handler.onData(chunk)
82
+ return super.onData(chunk)
85
83
  }
86
84
 
87
85
  onComplete() {
@@ -101,7 +99,7 @@ class Handler extends DecoratorHandler {
101
99
  'upstream request completed',
102
100
  )
103
101
 
104
- return this.#handler.onComplete()
102
+ super.onComplete()
105
103
  }
106
104
 
107
105
  onError(err) {
@@ -126,7 +124,7 @@ class Handler extends DecoratorHandler {
126
124
  this.#logger.error(data, 'upstream request failed')
127
125
  }
128
126
 
129
- return this.#handler.onError(err)
127
+ super.onError(err)
130
128
  }
131
129
  }
132
130
 
@@ -3,18 +3,16 @@ import createError from 'http-errors'
3
3
  import { DecoratorHandler, parseHeaders } from '../utils.js'
4
4
 
5
5
  class Handler extends DecoratorHandler {
6
- #handler
7
6
  #opts
8
7
 
9
8
  constructor(proxyOpts, { handler }) {
10
9
  super(handler)
11
10
 
12
- this.#handler = handler
13
11
  this.#opts = proxyOpts
14
12
  }
15
13
 
16
14
  onUpgrade(statusCode, rawHeaders, socket, headers = parseHeaders(rawHeaders)) {
17
- return this.#handler.onUpgrade(
15
+ super.onUpgrade(
18
16
  statusCode,
19
17
  reduceHeaders(
20
18
  {
@@ -34,7 +32,7 @@ class Handler extends DecoratorHandler {
34
32
  }
35
33
 
36
34
  onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
37
- return this.#handler.onHeaders(
35
+ return super.onHeaders(
38
36
  statusCode,
39
37
  reduceHeaders(
40
38
  {
@@ -50,7 +48,7 @@ class Handler extends DecoratorHandler {
50
48
  [],
51
49
  ),
52
50
  resume,
53
- statusMessage,
51
+ null,
54
52
  )
55
53
  }
56
54
  }
@@ -5,27 +5,25 @@ const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
5
5
 
6
6
  class Handler extends DecoratorHandler {
7
7
  #dispatch
8
- #handler
9
8
  #opts
10
9
  #maxCount
11
10
 
12
- #abort = null
11
+ #abort
13
12
  #aborted = false
14
13
  #reason = null
15
14
  #headersSent = false
16
15
  #count = 0
17
- #location = ''
16
+ #location
18
17
  #history = []
19
18
 
20
19
  constructor(opts, { dispatch, handler }) {
21
20
  super(handler)
22
21
 
23
22
  this.#dispatch = dispatch
24
- this.#handler = handler
25
23
  this.#opts = opts
26
24
  this.#maxCount = Number.isFinite(opts.follow) ? opts.follow : (opts.follow?.count ?? 0)
27
25
 
28
- this.#handler.onConnect((reason) => {
26
+ super.onConnect((reason) => {
29
27
  this.#aborted = true
30
28
  if (this.#abort) {
31
29
  this.#abort(reason)
@@ -44,14 +42,14 @@ class Handler extends DecoratorHandler {
44
42
  }
45
43
 
46
44
  onUpgrade(statusCode, rawHeaders, socket, headers) {
47
- return this.#handler.onUpgrade(statusCode, rawHeaders, socket, headers)
45
+ super.onUpgrade(statusCode, rawHeaders, socket, headers)
48
46
  }
49
47
 
50
- onHeaders(statusCode, rawHeaders, resume, statusText, headers = parseHeaders(rawHeaders)) {
48
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
51
49
  if (redirectableStatusCodes.indexOf(statusCode) === -1) {
52
50
  assert(!this.#headersSent)
53
51
  this.#headersSent = true
54
- return this.#handler.onHeaders(statusCode, null, resume, statusText, headers)
52
+ return super.onHeaders(statusCode, null, resume, null, headers)
55
53
  }
56
54
 
57
55
  if (isDisturbed(this.#opts.body)) {
@@ -71,7 +69,7 @@ class Handler extends DecoratorHandler {
71
69
  if (!this.#opts.follow(this.#location, this.#count, this.#opts)) {
72
70
  assert(!this.#headersSent)
73
71
  this.#headersSent = true
74
- return this.#handler.onHeaders(statusCode, null, resume, statusText, headers)
72
+ return super.onHeaders(statusCode, null, resume, null, headers)
75
73
  }
76
74
  } else {
77
75
  if (this.#count >= this.#maxCount) {
@@ -128,7 +126,7 @@ class Handler extends DecoratorHandler {
128
126
  servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
129
127
  */
130
128
  } else {
131
- return this.#handler.onData(chunk)
129
+ return super.onData(chunk)
132
130
  }
133
131
  }
134
132
 
@@ -147,12 +145,12 @@ class Handler extends DecoratorHandler {
147
145
 
148
146
  this.#dispatch(this.#opts, this)
149
147
  } else {
150
- return this.#handler.onComplete(trailers)
148
+ super.onComplete(trailers)
151
149
  }
152
150
  }
153
151
 
154
152
  onError(error) {
155
- return this.#handler.onError(error)
153
+ super.onError(error)
156
154
  }
157
155
  }
158
156
 
@@ -2,21 +2,21 @@ import createHttpError from 'http-errors'
2
2
  import { DecoratorHandler, parseHeaders } from '../utils.js'
3
3
 
4
4
  class Handler extends DecoratorHandler {
5
- #handler
6
-
7
5
  #statusCode = 0
8
6
  #contentType
9
7
  #decoder
10
8
  #headers
11
9
  #body = ''
12
10
  #opts
13
- #errored = false
14
11
 
15
12
  constructor(opts, { handler }) {
16
13
  super(handler)
17
14
 
18
15
  this.#opts = opts
19
- this.#handler = handler
16
+ }
17
+
18
+ #checkContentType(contentType) {
19
+ return (this.#contentType ?? '').indexOf(contentType) === 0
20
20
  }
21
21
 
22
22
  onConnect(abort) {
@@ -25,9 +25,8 @@ class Handler extends DecoratorHandler {
25
25
  this.#decoder = null
26
26
  this.#headers = null
27
27
  this.#body = ''
28
- this.#errored = false
29
28
 
30
- return this.#handler.onConnect(abort)
29
+ super.onConnect(abort)
31
30
  }
32
31
 
33
32
  onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
@@ -36,28 +35,27 @@ class Handler extends DecoratorHandler {
36
35
  this.#contentType = headers['content-type']
37
36
 
38
37
  if (this.#statusCode < 400) {
39
- return this.#handler.onHeaders(statusCode, null, resume, statusMessage, headers)
38
+ return super.onHeaders(statusCode, null, resume, null, headers)
40
39
  }
41
40
 
42
- // TODO (fix): Check content length
43
- if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
41
+ if (this.#checkContentType('application/json') || this.#checkContentType('text/plain')) {
44
42
  this.#decoder = new TextDecoder('utf-8')
45
43
  }
46
44
  }
47
45
 
48
46
  onData(chunk) {
49
- if (this.#statusCode >= 400) {
50
- this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
51
- } else {
52
- return this.#handler.onData(chunk)
47
+ if (this.#statusCode < 400) {
48
+ return super.onData(chunk)
53
49
  }
50
+
51
+ this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
54
52
  }
55
53
 
56
54
  onComplete(rawTrailers) {
57
55
  if (this.#statusCode >= 400) {
58
56
  this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
59
57
 
60
- if (this.#contentType === 'application/json') {
58
+ if (this.#checkContentType('application/json')) {
61
59
  try {
62
60
  this.#body = JSON.parse(this.#body)
63
61
  } catch {
@@ -65,10 +63,7 @@ class Handler extends DecoratorHandler {
65
63
  }
66
64
  }
67
65
 
68
- this.#errored = true
69
-
70
66
  let err
71
-
72
67
  const stackTraceLimit = Error.stackTraceLimit
73
68
  Error.stackTraceLimit = 0
74
69
  try {
@@ -76,18 +71,15 @@ class Handler extends DecoratorHandler {
76
71
  } finally {
77
72
  Error.stackTraceLimit = stackTraceLimit
78
73
  }
79
- this.#handler.onError(this.#decorateError(err))
74
+
75
+ super.onError(this.#decorateError(err))
80
76
  } else {
81
- this.#handler.onComplete(rawTrailers)
77
+ super.onComplete(rawTrailers)
82
78
  }
83
79
  }
84
80
 
85
81
  onError(err) {
86
- if (this.#errored) {
87
- // Do nothing...
88
- } else {
89
- this.#handler.onError(this.#decorateError(err))
90
- }
82
+ super.onError(this.#decorateError(err))
91
83
  }
92
84
 
93
85
  #decorateError(err) {
@@ -123,13 +115,13 @@ class Handler extends DecoratorHandler {
123
115
  }
124
116
 
125
117
  return err
126
- } catch {
127
- return err
118
+ } catch (er) {
119
+ return new AggregateError([er, err])
128
120
  }
129
121
  }
130
122
  }
131
123
 
132
124
  export default () => (dispatch) => (opts, handler) =>
133
- opts.error !== false && opts.throwOnError !== false
125
+ opts.throwOnError !== false
134
126
  ? dispatch(opts, new Handler(opts, { handler }))
135
127
  : dispatch(opts, handler)