Spaces:
Paused
Paused
| const { | |
| InvalidArgumentError, | |
| NotSupportedError | |
| } = require('./errors') | |
| const assert = require('node:assert') | |
| const { | |
| isValidHTTPToken, | |
| isValidHeaderValue, | |
| isStream, | |
| destroy, | |
| isBuffer, | |
| isFormDataLike, | |
| isIterable, | |
| hasSafeIterator, | |
| isBlobLike, | |
| serializePathWithQuery, | |
| assertRequestHandler, | |
| getServerName, | |
| normalizedMethodRecords, | |
| getProtocolFromUrlString | |
| } = require('./util') | |
| const { channels } = require('./diagnostics.js') | |
| const { headerNameLowerCasedRecord } = require('./constants') | |
| // Verifies that a given path is valid does not contain control chars \x00 to \x20 | |
| const invalidPathRegex = /[^\u0021-\u00ff]/ | |
| function isValidContentLengthHeaderValue (val) { | |
| if (typeof val !== 'string' || val.length === 0) { | |
| return false | |
| } | |
| for (let i = 0; i < val.length; i++) { | |
| const charCode = val.charCodeAt(i) | |
| if (charCode < 48 || charCode > 57) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| const kHandler = Symbol('handler') | |
| class Request { | |
| constructor (origin, { | |
| path, | |
| method, | |
| body, | |
| headers, | |
| query, | |
| idempotent, | |
| blocking, | |
| upgrade, | |
| headersTimeout, | |
| bodyTimeout, | |
| reset, | |
| expectContinue, | |
| servername, | |
| throwOnError, | |
| maxRedirections, | |
| typeOfService | |
| }, handler) { | |
| if (typeof path !== 'string') { | |
| throw new InvalidArgumentError('path must be a string') | |
| } else if ( | |
| path[0] !== '/' && | |
| !(path.startsWith('http://') || path.startsWith('https://')) && | |
| method !== 'CONNECT' | |
| ) { | |
| throw new InvalidArgumentError('path must be an absolute URL or start with a slash') | |
| } else if (invalidPathRegex.test(path)) { | |
| throw new InvalidArgumentError('invalid request path') | |
| } | |
| if (typeof method !== 'string') { | |
| throw new InvalidArgumentError('method must be a string') | |
| } else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) { | |
| throw new InvalidArgumentError('invalid request method') | |
| } | |
| if (upgrade && typeof upgrade !== 'string') { | |
| throw new InvalidArgumentError('upgrade must be a string') | |
| } | |
| if (upgrade && !isValidHeaderValue(upgrade)) { | |
| throw new InvalidArgumentError('invalid upgrade header') | |
| } | |
| if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { | |
| throw new InvalidArgumentError('invalid headersTimeout') | |
| } | |
| if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { | |
| throw new InvalidArgumentError('invalid bodyTimeout') | |
| } | |
| if (reset != null && typeof reset !== 'boolean') { | |
| throw new InvalidArgumentError('invalid reset') | |
| } | |
| if (expectContinue != null && typeof expectContinue !== 'boolean') { | |
| throw new InvalidArgumentError('invalid expectContinue') | |
| } | |
| if (throwOnError != null) { | |
| throw new InvalidArgumentError('invalid throwOnError') | |
| } | |
| if (maxRedirections != null && maxRedirections !== 0) { | |
| throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor') | |
| } | |
| if (typeOfService != null && (!Number.isInteger(typeOfService) || typeOfService < 0 || typeOfService > 255)) { | |
| throw new InvalidArgumentError('typeOfService must be an integer between 0 and 255') | |
| } | |
| this.headersTimeout = headersTimeout | |
| this.bodyTimeout = bodyTimeout | |
| this.method = method | |
| this.typeOfService = typeOfService ?? 0 | |
| this.abort = null | |
| if (body == null) { | |
| this.body = null | |
| } else if (isStream(body)) { | |
| this.body = body | |
| const rState = this.body._readableState | |
| if (!rState || !rState.autoDestroy) { | |
| this.endHandler = function autoDestroy () { | |
| destroy(this) | |
| } | |
| this.body.on('end', this.endHandler) | |
| } | |
| this.errorHandler = err => { | |
| if (this.abort) { | |
| this.abort(err) | |
| } else { | |
| this.error = err | |
| } | |
| } | |
| this.body.on('error', this.errorHandler) | |
| } else if (isBuffer(body)) { | |
| this.body = body.byteLength ? body : null | |
| } else if (ArrayBuffer.isView(body)) { | |
| this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null | |
| } else if (body instanceof ArrayBuffer) { | |
| this.body = body.byteLength ? Buffer.from(body) : null | |
| } else if (typeof body === 'string') { | |
| this.body = body.length ? Buffer.from(body) : null | |
| } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) { | |
| this.body = body | |
| } else { | |
| throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') | |
| } | |
| this.completed = false | |
| this.aborted = false | |
| this.upgrade = upgrade || null | |
| this.path = query ? serializePathWithQuery(path, query) : path | |
| // TODO: shall we maybe standardize it to an URL object? | |
| this.origin = origin | |
| this.protocol = getProtocolFromUrlString(origin) | |
| this.idempotent = idempotent == null | |
| ? method === 'HEAD' || method === 'GET' | |
| : idempotent | |
| this.blocking = blocking ?? this.method !== 'HEAD' | |
| this.reset = reset == null ? null : reset | |
| this.host = null | |
| this.contentLength = null | |
| this.contentType = null | |
| this.headers = [] | |
| // Only for H2 | |
| this.expectContinue = expectContinue != null ? expectContinue : false | |
| if (Array.isArray(headers)) { | |
| if (headers.length % 2 !== 0) { | |
| throw new InvalidArgumentError('headers array must be even') | |
| } | |
| for (let i = 0; i < headers.length; i += 2) { | |
| processHeader(this, headers[i], headers[i + 1]) | |
| } | |
| } else if (headers && typeof headers === 'object') { | |
| if (hasSafeIterator(headers)) { | |
| for (const header of headers) { | |
| if (!Array.isArray(header) || header.length !== 2) { | |
| throw new InvalidArgumentError('headers must be in key-value pair format') | |
| } | |
| processHeader(this, header[0], header[1]) | |
| } | |
| } else { | |
| const keys = Object.keys(headers) | |
| for (let i = 0; i < keys.length; ++i) { | |
| processHeader(this, keys[i], headers[keys[i]]) | |
| } | |
| } | |
| } else if (headers != null) { | |
| throw new InvalidArgumentError('headers must be an object or an array') | |
| } | |
| assertRequestHandler(handler, method, upgrade) | |
| this.servername = servername || getServerName(this.host) || null | |
| this[kHandler] = handler | |
| if (channels.create.hasSubscribers) { | |
| channels.create.publish({ request: this }) | |
| } | |
| } | |
| onBodySent (chunk) { | |
| if (channels.bodyChunkSent.hasSubscribers) { | |
| channels.bodyChunkSent.publish({ request: this, chunk }) | |
| } | |
| if (this[kHandler].onBodySent) { | |
| try { | |
| return this[kHandler].onBodySent(chunk) | |
| } catch (err) { | |
| this.abort(err) | |
| } | |
| } | |
| } | |
| onRequestSent () { | |
| if (channels.bodySent.hasSubscribers) { | |
| channels.bodySent.publish({ request: this }) | |
| } | |
| if (this[kHandler].onRequestSent) { | |
| try { | |
| return this[kHandler].onRequestSent() | |
| } catch (err) { | |
| this.abort(err) | |
| } | |
| } | |
| } | |
| onConnect (abort) { | |
| assert(!this.aborted) | |
| assert(!this.completed) | |
| if (this.error) { | |
| abort(this.error) | |
| } else { | |
| this.abort = abort | |
| return this[kHandler].onConnect(abort) | |
| } | |
| } | |
| onResponseStarted () { | |
| return this[kHandler].onResponseStarted?.() | |
| } | |
| onHeaders (statusCode, headers, resume, statusText) { | |
| assert(!this.aborted) | |
| assert(!this.completed) | |
| if (channels.headers.hasSubscribers) { | |
| channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) | |
| } | |
| try { | |
| return this[kHandler].onHeaders(statusCode, headers, resume, statusText) | |
| } catch (err) { | |
| this.abort(err) | |
| } | |
| } | |
| onData (chunk) { | |
| assert(!this.aborted) | |
| assert(!this.completed) | |
| if (channels.bodyChunkReceived.hasSubscribers) { | |
| channels.bodyChunkReceived.publish({ request: this, chunk }) | |
| } | |
| try { | |
| return this[kHandler].onData(chunk) | |
| } catch (err) { | |
| this.abort(err) | |
| return false | |
| } | |
| } | |
| onUpgrade (statusCode, headers, socket) { | |
| assert(!this.aborted) | |
| assert(!this.completed) | |
| return this[kHandler].onUpgrade(statusCode, headers, socket) | |
| } | |
| onComplete (trailers) { | |
| this.onFinally() | |
| assert(!this.aborted) | |
| assert(!this.completed) | |
| this.completed = true | |
| if (channels.trailers.hasSubscribers) { | |
| channels.trailers.publish({ request: this, trailers }) | |
| } | |
| try { | |
| return this[kHandler].onComplete(trailers) | |
| } catch (err) { | |
| // TODO (fix): This might be a bad idea? | |
| this.onError(err) | |
| } | |
| } | |
| onError (error) { | |
| this.onFinally() | |
| if (channels.error.hasSubscribers) { | |
| channels.error.publish({ request: this, error }) | |
| } | |
| if (this.aborted) { | |
| return | |
| } | |
| this.aborted = true | |
| return this[kHandler].onError(error) | |
| } | |
| onFinally () { | |
| if (this.errorHandler) { | |
| this.body.off('error', this.errorHandler) | |
| this.errorHandler = null | |
| } | |
| if (this.endHandler) { | |
| this.body.off('end', this.endHandler) | |
| this.endHandler = null | |
| } | |
| } | |
| addHeader (key, value) { | |
| processHeader(this, key, value) | |
| return this | |
| } | |
| } | |
| function processHeader (request, key, val) { | |
| if (val && (typeof val === 'object' && !Array.isArray(val))) { | |
| throw new InvalidArgumentError(`invalid ${key} header`) | |
| } else if (val === undefined) { | |
| return | |
| } | |
| let headerName = headerNameLowerCasedRecord[key] | |
| if (headerName === undefined) { | |
| headerName = key.toLowerCase() | |
| if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) { | |
| throw new InvalidArgumentError('invalid header key') | |
| } | |
| } | |
| if (Array.isArray(val)) { | |
| const arr = [] | |
| for (let i = 0; i < val.length; i++) { | |
| if (typeof val[i] === 'string') { | |
| if (!isValidHeaderValue(val[i])) { | |
| throw new InvalidArgumentError(`invalid ${key} header`) | |
| } | |
| arr.push(val[i]) | |
| } else if (val[i] === null) { | |
| arr.push('') | |
| } else if (typeof val[i] === 'object') { | |
| throw new InvalidArgumentError(`invalid ${key} header`) | |
| } else { | |
| arr.push(`${val[i]}`) | |
| } | |
| } | |
| val = arr | |
| } else if (typeof val === 'string') { | |
| if (!isValidHeaderValue(val)) { | |
| throw new InvalidArgumentError(`invalid ${key} header`) | |
| } | |
| } else if (val === null) { | |
| val = '' | |
| } else { | |
| val = `${val}` | |
| } | |
| if (headerName === 'host') { | |
| if (request.host !== null) { | |
| throw new InvalidArgumentError('duplicate host header') | |
| } | |
| if (typeof val !== 'string') { | |
| throw new InvalidArgumentError('invalid host header') | |
| } | |
| // Consumed by Client | |
| request.host = val | |
| } else if (headerName === 'content-length') { | |
| if (request.contentLength !== null) { | |
| throw new InvalidArgumentError('duplicate content-length header') | |
| } | |
| if (!isValidContentLengthHeaderValue(val)) { | |
| throw new InvalidArgumentError('invalid content-length header') | |
| } | |
| request.contentLength = parseInt(val, 10) | |
| } else if (request.contentType === null && headerName === 'content-type') { | |
| request.contentType = val | |
| request.headers.push(key, val) | |
| } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') { | |
| throw new InvalidArgumentError(`invalid ${headerName} header`) | |
| } else if (headerName === 'connection') { | |
| // Per RFC 7230 Section 6.1, Connection header can contain | |
| // a comma-separated list of connection option tokens (header names) | |
| const value = typeof val === 'string' ? val : null | |
| if (value === null) { | |
| throw new InvalidArgumentError('invalid connection header') | |
| } | |
| for (const token of value.toLowerCase().split(',')) { | |
| const trimmed = token.trim() | |
| if (!isValidHTTPToken(trimmed)) { | |
| throw new InvalidArgumentError('invalid connection header') | |
| } | |
| if (trimmed === 'close') { | |
| request.reset = true | |
| } | |
| } | |
| } else if (headerName === 'expect') { | |
| throw new NotSupportedError('expect header not supported') | |
| } else { | |
| request.headers.push(key, val) | |
| } | |
| } | |
| module.exports = Request | |