183 lines
4.6 KiB
JavaScript
183 lines
4.6 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const assert = require('node:assert')
|
||
|
const { Readable } = require('./readable')
|
||
|
const { InvalidArgumentError } = require('../core/errors')
|
||
|
const util = require('../core/util')
|
||
|
const { getResolveErrorBodyCallback } = require('./util')
|
||
|
const { AsyncResource } = require('node:async_hooks')
|
||
|
const { addSignal, removeSignal } = require('./abort-signal')
|
||
|
|
||
|
class RequestHandler extends AsyncResource {
|
||
|
constructor (opts, callback) {
|
||
|
if (!opts || typeof opts !== 'object') {
|
||
|
throw new InvalidArgumentError('invalid opts')
|
||
|
}
|
||
|
|
||
|
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts
|
||
|
|
||
|
try {
|
||
|
if (typeof callback !== 'function') {
|
||
|
throw new InvalidArgumentError('invalid callback')
|
||
|
}
|
||
|
|
||
|
if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
|
||
|
throw new InvalidArgumentError('invalid highWaterMark')
|
||
|
}
|
||
|
|
||
|
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
|
||
|
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
|
||
|
}
|
||
|
|
||
|
if (method === 'CONNECT') {
|
||
|
throw new InvalidArgumentError('invalid method')
|
||
|
}
|
||
|
|
||
|
if (onInfo && typeof onInfo !== 'function') {
|
||
|
throw new InvalidArgumentError('invalid onInfo callback')
|
||
|
}
|
||
|
|
||
|
super('UNDICI_REQUEST')
|
||
|
} catch (err) {
|
||
|
if (util.isStream(body)) {
|
||
|
util.destroy(body.on('error', util.nop), err)
|
||
|
}
|
||
|
throw err
|
||
|
}
|
||
|
|
||
|
this.responseHeaders = responseHeaders || null
|
||
|
this.opaque = opaque || null
|
||
|
this.callback = callback
|
||
|
this.res = null
|
||
|
this.abort = null
|
||
|
this.body = body
|
||
|
this.trailers = {}
|
||
|
this.context = null
|
||
|
this.onInfo = onInfo || null
|
||
|
this.throwOnError = throwOnError
|
||
|
this.highWaterMark = highWaterMark
|
||
|
|
||
|
if (util.isStream(body)) {
|
||
|
body.on('error', (err) => {
|
||
|
this.onError(err)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
addSignal(this, signal)
|
||
|
}
|
||
|
|
||
|
onConnect (abort, context) {
|
||
|
if (this.reason) {
|
||
|
abort(this.reason)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
assert(this.callback)
|
||
|
|
||
|
this.abort = abort
|
||
|
this.context = context
|
||
|
}
|
||
|
|
||
|
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
||
|
const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this
|
||
|
|
||
|
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
||
|
|
||
|
if (statusCode < 200) {
|
||
|
if (this.onInfo) {
|
||
|
this.onInfo({ statusCode, headers })
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
|
||
|
const contentType = parsedHeaders['content-type']
|
||
|
const contentLength = parsedHeaders['content-length']
|
||
|
const body = new Readable({ resume, abort, contentType, contentLength, highWaterMark })
|
||
|
|
||
|
this.callback = null
|
||
|
this.res = body
|
||
|
if (callback !== null) {
|
||
|
if (this.throwOnError && statusCode >= 400) {
|
||
|
this.runInAsyncScope(getResolveErrorBodyCallback, null,
|
||
|
{ callback, body, contentType, statusCode, statusMessage, headers }
|
||
|
)
|
||
|
} else {
|
||
|
this.runInAsyncScope(callback, null, null, {
|
||
|
statusCode,
|
||
|
headers,
|
||
|
trailers: this.trailers,
|
||
|
opaque,
|
||
|
body,
|
||
|
context
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
onData (chunk) {
|
||
|
const { res } = this
|
||
|
return res.push(chunk)
|
||
|
}
|
||
|
|
||
|
onComplete (trailers) {
|
||
|
const { res } = this
|
||
|
|
||
|
removeSignal(this)
|
||
|
|
||
|
util.parseHeaders(trailers, this.trailers)
|
||
|
|
||
|
res.push(null)
|
||
|
}
|
||
|
|
||
|
onError (err) {
|
||
|
const { res, callback, body, opaque } = this
|
||
|
|
||
|
removeSignal(this)
|
||
|
|
||
|
if (callback) {
|
||
|
// TODO: Does this need queueMicrotask?
|
||
|
this.callback = null
|
||
|
queueMicrotask(() => {
|
||
|
this.runInAsyncScope(callback, null, err, { opaque })
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (res) {
|
||
|
this.res = null
|
||
|
// Ensure all queued handlers are invoked before destroying res.
|
||
|
queueMicrotask(() => {
|
||
|
util.destroy(res, err)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (body) {
|
||
|
this.body = null
|
||
|
util.destroy(body, err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function request (opts, callback) {
|
||
|
if (callback === undefined) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
request.call(this, opts, (err, data) => {
|
||
|
return err ? reject(err) : resolve(data)
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
this.dispatch(opts, new RequestHandler(opts, callback))
|
||
|
} catch (err) {
|
||
|
if (typeof callback !== 'function') {
|
||
|
throw err
|
||
|
}
|
||
|
const opaque = opts?.opaque
|
||
|
queueMicrotask(() => callback(err, { opaque }))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = request
|
||
|
module.exports.RequestHandler = RequestHandler
|