'use strict'; const { InvalidArgumentError, NotSupportedError } = require('./errors'); const assert = require('node:assert'); const { isValidHTTPToken, isValidHeaderValue, isStream, destroy, isBuffer, isFormDataLike, isIterable, isBlobLike, serializePathWithQuery, assertRequestHandler, getServerName, normalizedMethodRecords, } = 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]/; const kHandler = Symbol('handler'); class Request { constructor( origin, { path, method, body, headers, query, idempotent, blocking, upgrade, headersTimeout, bodyTimeout, reset, expectContinue, servername, throwOnError, }, 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 ( 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'); } this.headersTimeout = headersTimeout; this.bodyTimeout = bodyTimeout; this.method = method; 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; this.origin = 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 (headers[Symbol.iterator]) { 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 (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); 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 (request.host === null && headerName === 'host') { if (typeof val !== 'string') { throw new InvalidArgumentError('invalid host header'); } // Consumed by Client request.host = val; } else if ( request.contentLength === null && headerName === 'content-length' ) { request.contentLength = parseInt(val, 10); if (!Number.isFinite(request.contentLength)) { throw new InvalidArgumentError('invalid content-length header'); } } 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') { const value = typeof val === 'string' ? val.toLowerCase() : null; if (value !== 'close' && value !== 'keep-alive') { throw new InvalidArgumentError('invalid connection header'); } if (value === 'close') { request.reset = true; } } else if (headerName === 'expect') { throw new NotSupportedError('expect header not supported'); } else { request.headers.push(key, val); } } module.exports = Request;