Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/docs/api/Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ The response returned an error status code. This is raised, for example, when th
* `code` {string} Always `'UND_ERR_RESPONSE'`.
* `statusCode` {number} The HTTP status code of the response.
* `body` {Object|string|null} The response body.
* `bodyTruncated` {boolean} Whether `body` was truncated by a configured limit.
* `headers` {Object|string[]|null} The response headers.

### `new ResponseError(message, code, options)`
Expand All @@ -445,6 +446,7 @@ The response returned an error status code. This is raised, for example, when th
* `options` {Object}
* `headers` {Object|string[]|null} The response headers. (optional)
* `body` {Object|string|null} The response body. (optional)
* `bodyTruncated` {boolean} Whether `body` was truncated. **Default:** `false`.

## Class: `ProxyConnectionError`

Expand Down
7 changes: 5 additions & 2 deletions docs/docs/api/Interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,11 @@ The error body is automatically decoded for `application/json` and

**Parameters**

* `opts` {Object} (optional) — currently reserved for future use; may be
omitted.
* `opts` {Object} (optional)
* `maxSize` {number} Maximum number of response body bytes to include in the
error. If the body exceeds this limit, the resulting `ResponseError`
contains the retained prefix with `bodyTruncated` set to `true`. Set to `0`
to disable the limit. **Default:** `1048576` (1 MiB).

**Returns:** {Dispatcher.DispatcherComposeInterceptor}

Expand Down
3 changes: 2 additions & 1 deletion lib/core/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,13 +365,14 @@ class RequestRetryError extends UndiciError {

const kResponseError = Symbol.for('undici.error.UND_ERR_RESPONSE')
class ResponseError extends UndiciError {
constructor (message, code, { headers, body }) {
constructor (message, code, { headers, body, bodyTruncated = false }) {
super(message)
this.name = 'ResponseError'
this.message = message || 'Response error'
this.code = 'UND_ERR_RESPONSE'
this.statusCode = code
this.body = body
this.bodyTruncated = bodyTruncated
this.headers = headers
}

Expand Down
100 changes: 72 additions & 28 deletions lib/interceptor/response-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@

// const { parseHeaders } = require('../core/util')
const DecoratorHandler = require('../handler/decorator-handler')
const { ResponseError } = require('../core/errors')
const { InvalidArgumentError, ResponseError } = require('../core/errors')

const defaultMaxSize = 1024 * 1024

class ResponseErrorHandler extends DecoratorHandler {
#statusCode
#contentType
#decoder
#headers
#body
#bodySize
#bodyTruncated
#maxSize

constructor (_opts, { handler }) {
constructor ({ maxSize }, { handler }) {
super(handler)
this.#maxSize = maxSize
}

#checkContentType (contentType) {
Expand All @@ -25,6 +31,8 @@ class ResponseErrorHandler extends DecoratorHandler {
this.#decoder = null
this.#headers = null
this.#body = ''
this.#bodySize = 0
this.#bodyTruncated = false

return super.onRequestStart(controller, context)
}
Expand All @@ -43,39 +51,69 @@ class ResponseErrorHandler extends DecoratorHandler {
}
}

#createResponseError () {
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
this.#decoder = null

if (!this.#bodyTruncated && this.#checkContentType('application/json')) {
try {
this.#body = JSON.parse(this.#body)
} catch {
// Do nothing...
}
}

let err
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 0
try {
err = new ResponseError('Response Error', this.#statusCode, {
body: this.#body,
bodyTruncated: this.#bodyTruncated,
headers: this.#headers
})
} finally {
Error.stackTraceLimit = stackTraceLimit
}

return err
}

onResponseData (controller, chunk) {
if (this.#statusCode < 400) {
return super.onResponseData(controller, chunk)
}

this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
if (this.#decoder == null) {
return
}

if (this.#maxSize === 0) {
this.#body += this.#decoder.decode(chunk, { stream: true })
return
}

const remaining = this.#maxSize - this.#bodySize

if (chunk.length <= remaining) {
this.#bodySize += chunk.length
this.#body += this.#decoder.decode(chunk, { stream: true })
return
}

if (remaining > 0) {
this.#body += this.#decoder.decode(chunk.subarray(0, remaining), { stream: true })
this.#bodySize = this.#maxSize
}

this.#bodyTruncated = true
this.#body += this.#decoder.decode(undefined, { stream: false })
this.#decoder = null
}

onResponseEnd (controller, trailers) {
if (this.#statusCode >= 400) {
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''

if (this.#checkContentType('application/json')) {
try {
this.#body = JSON.parse(this.#body)
} catch {
// Do nothing...
}
}

let err
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 0
try {
err = new ResponseError('Response Error', this.#statusCode, {
body: this.#body,
headers: this.#headers
})
} finally {
Error.stackTraceLimit = stackTraceLimit
}

super.onResponseError(controller, err)
super.onResponseError(controller, this.#createResponseError())
} else {
super.onResponseEnd(controller, trailers)
}
Expand All @@ -86,10 +124,16 @@ class ResponseErrorHandler extends DecoratorHandler {
}
}

module.exports = () => {
module.exports = (opts = {}) => {
const { maxSize = defaultMaxSize } = opts

if (!Number.isSafeInteger(maxSize) || maxSize < 0) {
throw new InvalidArgumentError('maxSize must be a non-negative integer')
}

return (dispatch) => {
return function Intercept (opts, handler) {
return dispatch(opts, new ResponseErrorHandler(opts, { handler }))
return dispatch(opts, new ResponseErrorHandler({ maxSize }, { handler }))
}
}
}
Loading
Loading