aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/node_modules/undici/lib/handler
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/undici/lib/handler')
-rw-r--r--node_modules/undici/lib/handler/retry-handler.js71
1 files changed, 67 insertions, 4 deletions
diff --git a/node_modules/undici/lib/handler/retry-handler.js b/node_modules/undici/lib/handler/retry-handler.js
index 5d1ccf0..7084c2b 100644
--- a/node_modules/undici/lib/handler/retry-handler.js
+++ b/node_modules/undici/lib/handler/retry-handler.js
@@ -15,6 +15,28 @@ function calculateRetryAfterHeader (retryAfter) {
return new Date(retryAfter).getTime() - current
}
+function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
+ const contentLength = headers['content-length']
+ if (contentLength == null) {
+ return null
+ }
+
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
+ return null
+ }
+
+ const length = Number(contentLength)
+ const expectedLength = range.end - range.start + 1
+ if (!Number.isFinite(length) || length !== expectedLength) {
+ return new RequestRetryError('Content-Length mismatch', statusCode, {
+ headers,
+ data: { count: retryCount }
+ })
+ }
+
+ return null
+}
+
class RetryHandler {
constructor (opts, handlers) {
const { retryOptions, ...dispatchOpts } = opts
@@ -68,6 +90,7 @@ class RetryHandler {
this.end = null
this.etag = null
this.resume = null
+ this.headersSent = false
// Handle possible onConnect duplication
this.handler.onConnect(reason => {
@@ -80,6 +103,20 @@ class RetryHandler {
})
}
+ checkpointResponseEnd (headers, resume) {
+ if (this.end == null && this.opts.method !== 'HEAD') {
+ const contentLength = headers['content-length']
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
+
+ assert(
+ this.end == null || Number.isFinite(this.end),
+ 'invalid content-length'
+ )
+ }
+
+ this.resume = this.end != null ? resume : null
+ }
+
onRequestSent () {
if (this.handler.onRequestSent) {
this.handler.onRequestSent()
@@ -168,7 +205,12 @@ class RetryHandler {
this.retryCount += 1
if (statusCode >= 300) {
- if (this.retryOpts.statusCodes.includes(statusCode) === false) {
+ // Only expose a response if no earlier attempt has reached the caller.
+ // Otherwise abort this attempt so the error settles the existing body
+ // instead of replacing it with a new response.
+ if (!this.headersSent && this.retryOpts.statusCodes.includes(statusCode) === false) {
+ this.headersSent = true
+ this.checkpointResponseEnd(headers, resume)
return this.handler.onHeaders(
statusCode,
rawHeaders,
@@ -229,10 +271,23 @@ class RetryHandler {
return false
}
+ const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
+ if (contentLengthError != null) {
+ this.abort(contentLengthError)
+ return false
+ }
+
const { start, size, end = size - 1 } = contentRange
- assert(this.start === start, 'content-range mismatch')
- assert(this.end == null || this.end === end, 'content-range mismatch')
+ if (this.start !== start || (this.end != null && this.end !== end)) {
+ this.abort(
+ new RequestRetryError('Content-Range mismatch', statusCode, {
+ headers,
+ data: { count: this.retryCount }
+ })
+ )
+ return false
+ }
this.resume = resume
return true
@@ -244,6 +299,7 @@ class RetryHandler {
const range = parseRangeHeader(headers['content-range'])
if (range == null) {
+ this.headersSent = true
return this.handler.onHeaders(
statusCode,
rawHeaders,
@@ -252,6 +308,12 @@ class RetryHandler {
)
}
+ const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
+ if (contentLengthError != null) {
+ this.abort(contentLengthError)
+ return false
+ }
+
const { start, size, end = size - 1 } = range
assert(
start != null && Number.isFinite(start),
@@ -276,6 +338,7 @@ class RetryHandler {
)
this.resume = resume
+ this.headersSent = true
this.etag = headers.etag != null ? headers.etag : null
// Weak etags are not useful for comparison nor cache
@@ -315,7 +378,7 @@ class RetryHandler {
}
onError (err) {
- if (this.aborted || isDisturbed(this.opts.body)) {
+ if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
return this.handler.onError(err)
}