UNPKG

light-my-request

Version:
291 lines (243 loc) 7.76 kB
'use strict' /* eslint no-prototype-builtins: 0 */ const { Readable, addAbortSignal } = require('node:stream') const util = require('node:util') const cookie = require('cookie') const assert = require('node:assert') const { createDeprecation } = require('process-warning') const parseURL = require('./parse-url') const { isFormDataLike, formDataToStream } = require('./form-data') const { EventEmitter } = require('node:events') // request.connectin deprecation https://nodejs.org/api/http.html#http_request_connection const FST_LIGHTMYREQUEST_DEP01 = createDeprecation({ name: 'FastifyDeprecationLightMyRequest', code: 'FST_LIGHTMYREQUEST_DEP01', message: 'You are accessing "request.connection", use "request.socket" instead.' }) /** * Get hostname:port * * @param {URL} parsedURL * @return {String} */ function hostHeaderFromURL (parsedURL) { return parsedURL.port ? parsedURL.host : parsedURL.hostname + (parsedURL.protocol === 'https:' ? ':443' : ':80') } /** * Mock socket object used to fake access to a socket for a request * * @constructor * @param {String} remoteAddress the fake address to show consumers of the socket */ class MockSocket extends EventEmitter { constructor (remoteAddress) { super() this.remoteAddress = remoteAddress } } /** * CustomRequest * * @constructor * @param {Object} options * @param {(Object|String)} options.url || options.path * @param {String} [options.method='GET'] * @param {String} [options.remoteAddress] * @param {Object} [options.cookies] * @param {Object} [options.headers] * @param {Object} [options.query] * @param {Object} [options.Request] * @param {any} [options.payload] */ function CustomRequest (options) { return new _CustomLMRRequest(this) function _CustomLMRRequest (obj) { Request.call(obj, { ...options, Request: undefined }) Object.assign(this, obj) for (const fn of Object.keys(Request.prototype)) { this.constructor.prototype[fn] = Request.prototype[fn] } util.inherits(this.constructor, options.Request) return this } } /** * Request * * @constructor * @param {Object} options * @param {(Object|String)} options.url || options.path * @param {String} [options.method='GET'] * @param {String} [options.remoteAddress] * @param {Object} [options.cookies] * @param {Object} [options.headers] * @param {Object} [options.query] * @param {any} [options.payload] */ function Request (options) { Readable.call(this, { autoDestroy: false }) const parsedURL = parseURL(options.url || options.path, options.query) this.url = parsedURL.pathname + parsedURL.search this.aborted = false this.httpVersionMajor = 1 this.httpVersionMinor = 1 this.httpVersion = '1.1' this.method = options.method ? options.method.toUpperCase() : 'GET' this.headers = {} this.rawHeaders = [] const headers = options.headers || {} for (const field in headers) { const fieldLowerCase = field.toLowerCase() if ( ( fieldLowerCase === 'user-agent' || fieldLowerCase === 'content-type' ) && headers[field] === undefined ) { this.headers[fieldLowerCase] = undefined continue } const value = headers[field] assert(value !== undefined, 'invalid value "undefined" for header ' + field) this.headers[fieldLowerCase] = '' + value } if (('user-agent' in this.headers) === false) { this.headers['user-agent'] = 'lightMyRequest' } this.headers.host = this.headers.host || options.authority || hostHeaderFromURL(parsedURL) if (options.cookies) { const { cookies } = options const cookieValues = Object.keys(cookies).map(key => cookie.serialize(key, cookies[key])) if (this.headers.cookie) { cookieValues.unshift(this.headers.cookie) } this.headers.cookie = cookieValues.join('; ') } this.socket = new MockSocket(options.remoteAddress || '127.0.0.1') Object.defineProperty(this, 'connection', { get () { FST_LIGHTMYREQUEST_DEP01() return this.socket }, configurable: true }) // we keep both payload and body for compatibility reasons let payload = options.payload || options.body || null let payloadResume = payload && typeof payload.resume === 'function' if (isFormDataLike(payload)) { const stream = formDataToStream(payload) payload = stream.stream payloadResume = true // we override the content-type this.headers['content-type'] = stream.contentType this.headers['transfer-encoding'] = 'chunked' } if (payload && typeof payload !== 'string' && !payloadResume && !Buffer.isBuffer(payload)) { payload = JSON.stringify(payload) if (('content-type' in this.headers) === false) { this.headers['content-type'] = 'application/json' } } // Set the content-length for the corresponding payload if none set if (payload && !payloadResume && !Object.hasOwn(this.headers, 'content-length')) { this.headers['content-length'] = (Buffer.isBuffer(payload) ? payload.length : Buffer.byteLength(payload)).toString() } for (const header of Object.keys(this.headers)) { this.rawHeaders.push(header, this.headers[header]) } // Use _lightMyRequest namespace to avoid collision with Node this._lightMyRequest = { payload, isDone: false, simulate: options.simulate || {}, payloadAsStream: options.payloadAsStream, signal: options.signal } const signal = options.signal /* c8 ignore next 3 */ if (signal) { addAbortSignal(signal, this) } { const payload = this._lightMyRequest.payload if (payload?._readableState) { // does quack like a modern stream this._read = readStream payload.on('error', (err) => { this.destroy(err) }) payload.on('end', () => { this.push(null) }) } else { // Stream v1 are handled in index.js synchronously this._read = readEverythingElse } } return this } function readStream () { const payload = this._lightMyRequest.payload let more = true let pushed = false let chunk while (more && (chunk = payload.read())) { pushed = true more = this.push(chunk) } // We set up a recursive 'readable' event only if we didn't read anything. // Otheriwse, the stream machinery will call _read() for us. if (more && !pushed) { this._lightMyRequest.payload.once('readable', this._read.bind(this)) } } function readEverythingElse () { setImmediate(() => { if (this._lightMyRequest.isDone) { // 'end' defaults to true if (this._lightMyRequest.simulate.end !== false) { this.push(null) } return } this._lightMyRequest.isDone = true if (this._lightMyRequest.payload) { if (this._lightMyRequest.simulate.split) { this.push(this._lightMyRequest.payload.slice(0, 1)) this.push(this._lightMyRequest.payload.slice(1)) } else { this.push(this._lightMyRequest.payload) } } if (this._lightMyRequest.simulate.error) { this.emit('error', new Error('Simulated')) } if (this._lightMyRequest.simulate.close) { this.emit('close') } // 'end' defaults to true if (this._lightMyRequest.simulate.end !== false) { this.push(null) } }) } util.inherits(Request, Readable) util.inherits(CustomRequest, Request) Request.prototype.destroy = function (error) { if (this.destroyed || this._lightMyRequest.isDone) return this.destroyed = true if (error) { this._error = true process.nextTick(() => this.emit('error', error)) } process.nextTick(() => this.emit('close')) } module.exports = Request module.exports.Request = Request module.exports.CustomRequest = CustomRequest