UNPKG

hyper-express

Version:

High performance Node.js webserver with a simple-to-use API powered by uWebsockets.js under the hood.

981 lines (834 loc) 40.5 kB
'use strict'; const util = require('util'); const cookie = require('cookie'); const stream = require('stream'); const busboy = require('busboy'); const querystring = require('querystring'); const signature = require('cookie-signature'); const MultipartField = require('../plugins/MultipartField.js'); const NodeRequest = require('../compatibility/NodeRequest.js'); const ExpressRequest = require('../compatibility/ExpressRequest.js'); const { inherit_prototype, array_buffer_to_string, copy_array_buffer_to_uint8array, } = require('../../shared/operators.js'); class Request { _locals; _paused = false; _request_ended = false; _raw_request = null; _raw_response = null; _method = ''; _url = ''; _path = ''; _query = ''; _remote_ip = ''; _remote_proxy_ip = ''; _cookies; _path_parameters; _query_parameters; /** * The route that this request is being handled by. */ route = null; /** * Underlying lazy initialized readable body stream. * @private */ _readable = null; /** * Returns whether all expected incoming request body chunks have been received. * @returns {Boolean} */ received = true; // Assume there is no body data to stream /** * Returns request headers from incoming request. * @returns {Object.<string, string>} */ headers = {}; /** * Creates a new HyperExpress request instance that wraps a uWS.HttpRequest instance. * * @param {import('../router/Route.js')} route * @param {import('uWebSockets.js').HttpRequest} raw_request */ constructor(route, raw_request) { // Store reference to the route of this request and the raw uWS.HttpResponse instance for certain operations this.route = route; this._raw_request = raw_request; // Cache request properties from uWS.HttpRequest as it is stack allocated and will be deallocated after this function returns this._query = raw_request.getQuery(); this._path = route.path || raw_request.getUrl(); this._method = route.method !== 'ANY' ? route.method : raw_request.getMethod(); // Cache request headers from uWS.HttpRequest as it is stack allocated and will be deallocated after this function returns raw_request.forEach((key, value) => (this.headers[key] = value)); // Cache the path parameters from the route pattern if any as uWS.HttpRequest will be deallocated after this function returns const num_path_parameters = route.path_parameters_key.length; if (num_path_parameters) { this._path_parameters = {}; for (let i = 0; i < num_path_parameters; i++) { const parts = route.path_parameters_key[i]; this._path_parameters[parts[0]] = raw_request.getParameter(parts[1]); } } } /* HyperExpress Methods */ /** * Returns the raw uWS.HttpRequest instance. * Note! This property is unsafe and should not be used unless you have no asynchronous code or you are accessing from the first top level synchronous middleware before any asynchronous code. * @returns {import('uWebSockets.js').HttpRequest} */ get raw() { return this._raw_request; } /** * Pauses the current request and flow of incoming body data. * @returns {Request} */ pause() { // Ensure there is content being streamed before pausing // Ensure that the stream is currently not paused before pausing if (!this._paused) { this._paused = true; this._raw_response.pause(); if (this._readable) return this._super_pause(); } return this; } /** * Resumes the current request and flow of incoming body data. * @returns {Request} */ resume() { // Ensure there is content being streamed before resuming // Ensure that the stream is currently paused before resuming if (this._paused) { this._paused = false; this._raw_response.resume(); if (this._readable) return this._super_resume(); } return this; } /** * Pipes the request body stream data to the provided destination stream with the provided set of options. * * @param {stream.Writable} destination * @param {stream.WritableOptions} options * @returns {Request} */ pipe(destination, options) { // Pipe the arguments to the request body stream this._super_pipe(destination, options); // Resume the request body stream as it will be in a paused state by default return this._super_resume(); } /** * Securely signs a value with provided secret and returns the signed value. * * @param {String} string * @param {String} secret * @returns {String} String OR undefined */ sign(string, secret) { return signature.sign(string, secret); } /** * Securely unsigns a value with provided secret and returns its original value upon successful verification. * * @param {String} signed_value * @param {String} secret * @returns {String=} String OR undefined */ unsign(signed_value, secret) { let unsigned_value = signature.unsign(signed_value, secret); if (unsigned_value !== false) return unsigned_value; } /* Body Parsing */ _body_parser_mode = 0; // 0 = none (awaiting mode), 1 = buffering (internal use), 2 = streaming (external use) _body_limit_bytes = 0; _body_received_bytes = 0; _body_expected_bytes = -1; // We initialize this to -1 as we will use this to ensure the uWS.HttpResponse.onData() is only called once _body_parser_flushing = false; _body_chunked_transfer = false; _body_parser_buffered; // This will hold the buffered chunks until the user decides to internally or externally consume the body data _body_parser_passthrough; // This will be a passthrough chunk acceptor callback used by internal body parsers /** * Begins parsing the incoming request body data within the provided limit in bytes. * NOTE: This method will be a no-op if there is no expected body based on the content-length header. * NOTE: This method can be called multiple times to update the bytes limit during the parsing process process. * * @private * @param {import('./Response.js')} response * @param {Number} bytes * @returns {Boolean} Returns whether this request is within the bytes limit and should be handled further. */ _body_parser_run(response, limit_bytes) { // Parse the content length into a number to ensure we have some body data to parse // Even though it can be NaN, the > 0 check will handle this case and ignore NaN // OR if the transfer-encoding header is chunked which means we will have to do a more inefficient chunked transfer const content_length = Number(this.headers['content-length']); const is_chunked_transfer = this.headers['transfer-encoding'] === 'chunked'; if (content_length > 0 || is_chunked_transfer) { // Determine if this is a first run meaning we have not began parsing the body yet const is_first_run = this._body_expected_bytes === -1; // Update the limit and expected body bytes as these will be used to check if we are within the limit this._body_limit_bytes = limit_bytes; this._body_expected_bytes = is_chunked_transfer ? 0 : content_length; // We use 0 to indicate we do not know the content length with chunked transfers // We want to track if we are expecting a chunked transfer so depending logic does not treat the 0 expected bytes as an empty body this._body_chunked_transfer = is_chunked_transfer; // Determine if this is a first time body parser run if (is_first_run) { // Set the request body to not received as we have some body data to parse this.received = false; // Ensure future runs do not trigger the handling process this._body_received_bytes = 0; // Initialize the array which will buffer the incoming chunks until a different parser mode is requested aka. user does something with the data this._body_parser_buffered = []; // Bind the uWS.HttpResponse.onData() event handler to begin accepting incoming body data this._raw_response.onData((chunk, is_last) => this._body_parser_on_chunk(response, chunk, is_last)); } // Enforce the limit as we may have a different limit than the previous run this._body_parser_enforce_limit(response); } // Return whether the body parser is actively parsing the incoming body data return !this._body_parser_flushing; } /** * Stops the body parser from accepting any more incoming body data. * @private */ _body_parser_stop() { // Return if we have no expected body length or already flushing the body if (this._body_expected_bytes === -1 || this._body_parser_flushing) return; // Mark the body parser as flushing to prevent any more incoming body data from being accepted this._body_parser_flushing = true; // Determine if we have a readable stream if (this._readable) { // Push an empty chunk to indicate the end of the stream this.push(null); // Resume the readable stream to ensure in case it was paused to flush the buffered chunks this.resume(); } } /** * Checks if the body parser so far is within the bytes limit and triggers the limit handling if reached. * * @private * @param {import('./Response.js')} response * @returns {Boolean} Returns `true` when the body limit has been reached. */ _body_parser_enforce_limit(response) { // Determine if we may have either received or are expecting more incoming bytes than the limit allows for const incoming_bytes = Math.max(this._body_received_bytes, this._body_expected_bytes); if (incoming_bytes > this._body_limit_bytes) { // Stop the body parser from accepting any more incoming body data this._body_parser_stop(); // Determine if we have not began sending a response yet and hence must send a response as soon as we can if (!response.initiated) { // If the server is instructed to do fast aborts, we will close the request immediately if (this.route.app._options.fast_abort) { response.close(); } else if (this.received) { // Otherwise, we will send a HTTP 413 Payload Too Large response once the request body has been fully flushed aka. received response.status(413).send(); } } return true; } return false; } /** * Processes incoming raw body data chunks from the uWS HttpResponse. * * @private * @param {import('./Response.js')} response * @param {ArrayBuffer} chunk * @param {Boolean} is_last */ _body_parser_on_chunk(response, chunk, is_last) { // If this chunk has no length and is not the last chunk, we will ignore it if (!chunk.byteLength && !is_last) return; // Increment the received bytes counter by the byteLength of the incoming chunk this._body_received_bytes += chunk.byteLength; // Determine if the body parser is active / not flushing if (!this._body_parser_flushing) { // Enforce the body parser limit as the number of incoming bytes may have exceeded the limit const limited = this._body_parser_enforce_limit(response); if (!limited) { // Process this chunk depending on the current body parser mode switch (this._body_parser_mode) { // Awaiting mode - Awaiting the user to do something with the incoming body data case 0: // Buffer a COPIED Uint8Array chunk from the uWS volatile ArrayBuffer chunk this._body_parser_buffered.push(copy_array_buffer_to_uint8array(chunk)); // If we have exceeded the Server.options.max_body_buffer number of buffered bytes, then pause the request to prevent more buffering if (this._body_received_bytes > this.app._options.max_body_buffer) this.pause(); break; // Buffering mode - Internal use only case 1: // Pass through the uWS volatile ArrayBuffer chunk to the passthrough callback as a volatile Uint8Array chunk this._body_parser_passthrough( // If this is a chunked transfer, we need to COPY the chunk as any passthrough consumer will have no immediate way of processing // hence this chunk needs to stick around across multiple cycles without being deallocated by uWS this._body_chunked_transfer ? copy_array_buffer_to_uint8array(chunk) : new Uint8Array(chunk), is_last ); break; // Streaming mode - External use only case 2: // Attempt to push a COPIED Uint8Array chunk from the uWS volatile ArrayBuffer chunk to the readable stream // Pause the request if we have reached the highWaterMark to prevent backpressure if (!this.push(copy_array_buffer_to_uint8array(chunk))) this.pause(); // If this is the last chunk, push a null chunk to indicate the end of the stream if (is_last) this.push(null); break; } } } // Determine if this is the last chunk of the incoming body data to perform final closing operations if (is_last) { // Mark the request as fully received as we have flushed all incoming body data this.received = true; // Emit the 'received' event that indicates how many bytes were received in total from the incoming body if (this._readable) this.emit('received', this._body_received_bytes); // Enforce the body parser limit one last time in case the request is waiting for the body to be flushed before sending a response if (this._body_parser_flushing) this._body_parser_enforce_limit(response); } } /** * Flushes the buffered chunks to the appropriate body parser mode. * @private */ _body_parser_flush_buffered() { // Determine if we have any buffered chunks if (this._body_parser_buffered) { // Determine the body parser mode to flush the buffered chunks to switch (this._body_parser_mode) { // Buffering mode - Internal use only case 1: // Iterate over the buffered chunks and pass them to the passthrough callback for (let i = 0; i < this._body_parser_buffered.length; i++) { this._body_parser_passthrough( this._body_parser_buffered[i], i === this._body_parser_buffered.length - 1 ? this.received : false ); } break; // Streaming mode - External use only case 2: // Iterate over the buffered chunks and push them to the readable stream for (const chunk of this._body_parser_buffered) { // Convert Uint8Array into a Buffer chunk const buffer = Buffer.from(chunk); // Push the buffer to the readable stream // We will ignore the return value as we are not handling backpressure here this.push(buffer); } // If the request has been received at this point already, we must also push a null chunk to indicate the end of the stream if (this.received) this.push(null); break; } } // Deallocate the buffered chunks array as they are no longer needed this._body_parser_buffered = null; // Resume the request in case we had paused the request due to having reached the max_body_buffer for this request this.resume(); } /** * This method is called when the underlying Readable stream is initialized and begins expecting incoming data. * @private */ _body_parser_stream_init() { // Set the body parser mode to stream mode this._body_parser_mode = 2; // Overwrite the underlying readable _read handler to resume the request when more chunks are requested // This will properly handle backpressure and prevent the request from being paused forever this._readable._read = () => this.resume(); // Flush the buffered chunks to the readable stream if we have any this._body_parser_flush_buffered(); } _received_data_promise; /** * Returns a single Uint8Array buffer which contains all incoming body data. * @private * @returns {Promise<Uint8Array>} */ _body_parser_get_received_data() { // Return the current promise if it exists if (this._received_data_promise) return this._received_data_promise; // If this is not a chunked transfer and we have no expected body length, we will return an empty buffer as we have no body data to parse if (!this._body_chunked_transfer && this._body_expected_bytes <= 0) return Promise.resolve(new Uint8Array(0)); // Create a new promise which will be resolved once all incoming body data has been received this._received_data_promise = new Promise((resolve) => { // Determine if this is a chunked transfer if (this._body_chunked_transfer) { // Since we don't know how many or how much each chunk will be, we have to store all the chunks // After all the chunks have been received, we will concatenate them into a single Uint8Array const chunks = []; // Define a passthrough callback which will be called for each incoming chunk this._body_parser_passthrough = (chunk, is_last) => { // Push the chunk to the chunks array chunks.push(chunk); // If this is the last chunk, call the callback with the body buffer if (is_last) { // Initialize a new Uint8Array of size received bytes let offset = 0; const buffer = new Uint8Array(this._body_received_bytes); for (const chunk of chunks) { // Write the chunk into the body buffer at the current offset buffer.set(chunk, offset); offset += chunk.byteLength; } // Resolve the promise with the body buffer resolve(buffer); } }; } else { // Initialize the full size body Uint8Array buffer based on the expected body length // We will copy all volatile chunk data onto this stable buffer for memory efficiency const buffer = new Uint8Array(this._body_expected_bytes); // Define a passthrough callback which will be called for each incoming chunk let offset = 0; this._body_parser_passthrough = (chunk, is_last) => { // Write the chunk into the body buffer at the current offset buffer.set(chunk, offset); // Increment the offset by the byteLength of the incoming chunk offset += chunk.byteLength; // If this is the last chunk, call the callback with the body buffer if (is_last) resolve(buffer); }; } // Set the body parser mode to buffering mode as we want to receive all incoming chunks through the passthrough callback this._body_parser_mode = 1; // Flush the buffered chunks so the passthrough callback receives all buffered data through its callback this._body_parser_flush_buffered(); }); // Return the data promise return this._received_data_promise; } _body_buffer; _buffer_promise; /** * Returns the incoming request body as a Buffer. * @returns {Promise<Buffer>} */ buffer() { // Check cache and return if body has already been parsed if (this._body_buffer) return Promise.resolve(this._body_buffer); // Initialize the buffer promise if it does not exist this._buffer_promise = new Promise((resolve) => this._body_parser_get_received_data().then((raw) => { // Convert the Uint8Array buffer into a Buffer this._body_buffer = Buffer.from(raw); // Resolve the buffer promise with the body buffer resolve(this._body_buffer); }) ); // Return the buffer promise return this._buffer_promise; } /** * Decodes the incoming request body as a String. * @private * @param {Uint8Array} uint8 * @param {string} encoding * @returns {string} */ _uint8_to_string(uint8, encoding = 'utf-8') { const decoder = new util.TextDecoder(encoding); return decoder.decode(uint8); } _body_text; _text_promise; /** * Downloads and parses the request body as a String. * @returns {Promise<string>} */ text() { // Resolve from cache if available if (this._body_text) return Promise.resolve(this._body_text); // Initialize the text promise if it does not exist this._text_promise = new Promise((resolve) => this._body_parser_get_received_data().then((raw) => { // Decode the Uint8Array buffer into a String this._body_text = this._uint8_to_string(raw); // Resolve the text promise with the body text resolve(this._body_text); }) ); // Return the text promise return this._text_promise; } _body_json; _json_promise; /** * Downloads and parses the request body as a JSON object. * Passing default_value as null will lead to the function throwing an exception if invalid JSON is received. * * @param {Any=} default_value Default: {} * @returns {Promise<Record>} */ json(default_value = {}) { // Return from cache if available if (this._body_json) return Promise.resolve(this._body_json); // Initialize the json promise if it does not exist this._json_promise = new Promise((resolve, reject) => this._body_parser_get_received_data().then((raw) => { // Decode the Uint8Array buffer into a String const text = this._uint8_to_string(raw); try { // Parse the text as JSON this._body_json = JSON.parse(text); } catch (error) { if (default_value) { // Use the default value if provided this._body_json = default_value; } else { reject(error); } } // Resolve the json promise with the body json resolve(this._body_json); }) ); // Return the json promise return this._json_promise; } _body_urlencoded; _urlencoded_promise; /** * Parses and resolves an Object of urlencoded values from body. * @returns {Promise<Record>} */ urlencoded() { // Return from cache if available if (this._body_urlencoded) return Promise.resolve(this._body_urlencoded); // Initialize the urlencoded promise if it does not exist this._urlencoded_promise = new Promise((resolve) => this._body_parser_get_received_data().then((raw) => { // Decode the Uint8Array buffer into a String const text = this._uint8_to_string(raw); // Parse the text as urlencoded this._body_urlencoded = querystring.parse(text); // Resolve the urlencoded promise with the body urlencoded resolve(this._body_urlencoded); }) ); // Return the urlencoded promise return this._urlencoded_promise; } _multipart_promise; /** * Handles incoming multipart fields from uploader and calls user specified handler with MultipartField. * * @private * @param {Function} handler * @param {String} name * @param {String|stream.Readable} value * @param {Object} info */ async _on_multipart_field(handler, name, value, info) { // Create a MultipartField instance with the incoming information const field = new MultipartField(name, value, info); // Check if a field is being handled by the user across a different exeuction if (this._multipart_promise instanceof Promise) { // Pause the request to prevent more fields from being received this.pause(); // Wait for this field to be handled if (this._multipart_promise) await this._multipart_promise; // Resume the request to accept more fields this.resume(); } // Determine if the handler is a synchronous function and returns a promise const output = handler(field); if (output instanceof Promise) { // Store the promise, so concurrent multipart fields can wait for it this._multipart_promise = output; // Hold the current exectution context until the promise resolves if (this._multipart_promise) await this._multipart_promise; // Clear the promise reference this._multipart_promise = null; } // Flush this field's file stream if it has not been consumed by the user in the handler execution // This is neccessary as defined in the Busboy documentation to prevent holding up the processing if (field.file && !field.file.stream.readableEnded) field.file.stream.resume(); } /** * @typedef {function(MultipartField):void} SyncMultipartHandler */ /** * @typedef {function(MultipartField):Promise<void>} AsyncMultipartHandler */ /** * @typedef {('PARTS_LIMIT_REACHED'|'FILES_LIMIT_REACHED'|'FIELDS_LIMIT_REACHED')} MultipartLimitReject */ /** * Downloads and parses incoming body as a multipart form. * This allows for easy consumption of fields, values and files. * * @param {busboy.BusboyConfig|SyncMultipartHandler|AsyncMultipartHandler} options * @param {(SyncMultipartHandler|AsyncMultipartHandler)=} handler * @returns {Promise<MultipartLimitReject|Error>} A promise which is resolved once all multipart fields have been processed */ multipart(options, handler) { // Migrate options to handler if no options object is provided by user if (typeof options == 'function') { handler = options; options = {}; } // Make a shallow copy of the options object options = Object.assign({}, options); // Inject the request headers into the busboy options if not provided if (!options.headers) options.headers = this.headers; // Ensure the provided handler is a function type if (typeof handler !== 'function') throw new Error('HyperExpress: Request.multipart(handler) -> handler must be a Function.'); // Resolve instantly if we have no readable body stream if (this.readableEnded) return Promise.resolve(); // Resolve instantly if we do not have a valid multipart content type header const content_type = this.headers['content-type']; if (!/^(multipart\/.+);(.*)$/i.test(content_type)) return Promise.resolve(); // Return a promise which will be resolved after all incoming multipart data has been processed const reference = this; return new Promise((resolve, reject) => { // Create a Busboy instance which will perform const uploader = busboy(options); // Create a function to finish the uploading process let finished = false; const finish = async (error) => { // Ensure we are not already finished if (finished) return; finished = true; // Determine if the caught error should be silenced let silent_error = false; if (error instanceof Error) { // Silence the BusBoy "Unexpected end of form" error // This usually happens when the client abruptly closes the connection if (error.message == 'Unexpected end of form') silent_error = true; } // Resolve/Reject the promise depending on whether an error occurred if (error && !silent_error) { // Reject the promise if an error occurred reject(error); } else { // Wait for any pending multipart handler exeuction to complete if (reference._multipart_promise) await reference._multipart_promise; // Resolve the promise if no error occurred resolve(); } // Stop the body parser from accepting any more incoming body data reference._body_parser_stop(); // Destroy the uploader instance uploader.destroy(); }; // Bind an 'error' event handler to emit errors uploader.once('error', finish); // Bind limit event handlers to reject as error code constants uploader.once('partsLimit', () => finish('PARTS_LIMIT_REACHED')); uploader.once('filesLimit', () => finish('FILES_LIMIT_REACHED')); uploader.once('fieldsLimit', () => finish('FIELDS_LIMIT_REACHED')); // Define a function to handle incoming multipart data const on_field = (name, value, info) => { // Catch and pipe any errors from the value readable stream to the finish function if (value instanceof stream.Readable) value.once('error', finish); // Call the user defined handler with the incoming multipart field // Catch and pipe any errors to the finish function reference._on_multipart_field(handler, name, value, info).catch(finish); }; // Bind a 'field' event handler to process each incoming field uploader.on('field', on_field); // Bind a 'file' event handler to process each incoming file uploader.on('file', on_field); // Bind a 'finish' event handler to resolve the upload promise uploader.once('close', () => { // Wait for any pending multipart handler exeuction to complete if (reference._multipart_promise) { // Wait for the pending promise to resolve // Use an anonymous callback for the .then() to prevent finish() from receving a resolved value which would lead to an error finish reference._multipart_promise.then(() => finish()).catch(finish); } else { finish(); } }); // Pipe the readable request stream into the busboy uploader reference.pipe(uploader); }); } /* HyperExpress Properties */ /** * Returns the request locals for this request. * @returns {Object.<string, any>} */ get locals() { // Initialize locals object if it does not exist if (!this._locals) this._locals = {}; return this._locals; } /** * Returns the HyperExpress.Server instance this Request object originated from. * @returns {import('../Server.js')} */ get app() { return this.route.app; } /** * Returns whether this request is in a paused state and thus not consuming any body chunks. * @returns {Boolean} */ get paused() { return this._paused; } /** * Returns HTTP request method for incoming request in uppercase. * @returns {String} */ get method() { // Enforce uppercase for the returned method value const uppercase = this._method.toUpperCase(); // For some reason, uWebsockets.js populates DELETE requests as DEL hence this translation return uppercase === 'DEL' ? 'DELETE' : uppercase; } /** * Returns full request url for incoming request (path + query). * @returns {String} */ get url() { // Return from cache if available if (this._url) return this._url; // Parse the incoming request url this._url = this._path + (this._query ? '?' + this._query : ''); // Return the url return this._url; } /** * Returns path for incoming request. * @returns {String} */ get path() { return this._path; } /** * Returns query for incoming request without the '?'. * @returns {String} */ get path_query() { return this._query; } /** * Returns request cookies from incoming request. * @returns {Object.<string, string>} */ get cookies() { // Return from cache if already parsed once if (this._cookies) return this._cookies; // Parse cookies from Cookie header and cache results const header = this.headers['cookie']; this._cookies = header ? cookie.parse(header) : {}; // Return the cookies return this._cookies; } /** * Returns path parameters from incoming request. * @returns {Object.<string, string>} */ get path_parameters() { return this._path_parameters || {}; } /** * Returns query parameters from incoming request. * @returns {Object.<string, string>} */ get query_parameters() { // Return from cache if already parsed once if (this._query_parameters) return this._query_parameters; // Parse query using querystring and cache results this._query_parameters = querystring.parse(this._query); return this._query_parameters; } /** * Returns remote IP address in string format from incoming request. * Note! You cannot call this method after the response has been sent or ended. * @returns {String} */ get ip() { // Resolve IP from cache if already resolved if (this._remote_ip) return this._remote_ip; // Ensure request has not ended yet if (this._request_ended) throw new Error('HyperExpress.Request.ip cannot be consumed after the Request/Response has ended.'); // Determine if we can trust intermediary proxy servers and have a x-forwarded-for header const x_forwarded_for = this.get('X-Forwarded-For'); const trust_proxy = this.route.app._options.trust_proxy; if (trust_proxy && x_forwarded_for) { // The first IP in the x-forwarded-for header is the client IP if we trust proxies this._remote_ip = x_forwarded_for.split(',')[0]; } else { // Use the uWS detected connection IP address as a fallback this._remote_ip = array_buffer_to_string(this._raw_response.getRemoteAddressAsText()); } // Return Remote IP return this._remote_ip; } /** * Returns remote proxy IP address in string format from incoming request. * Note! You cannot call this method after the response has been sent or ended. * @returns {String} */ get proxy_ip() { // Resolve IP from cache if already resolved if (this._remote_proxy_ip) return this._remote_proxy_ip; // Ensure request has not ended yet if (this._request_ended) throw new Error('HyperExpress.Request.proxy_ip cannot be consumed after the Request/Response has ended.'); // Parse and cache remote proxy IP from uWS this._remote_proxy_ip = array_buffer_to_string(this._raw_response.getProxiedRemoteAddressAsText()); // Return Remote Proxy IP return this._remote_proxy_ip; } /** * Throws an ERR_INCOMPATIBLE_CALL error with the provided property/method name. * @private */ _throw_unsupported(name) { throw new Error( `ERR_INCOMPATIBLE_CALL: One of your middlewares or route logic tried to call Request.${name} which is unsupported with HyperExpress.` ); } } // Inherit the compatibility classes inherit_prototype({ from: [NodeRequest.prototype, ExpressRequest.prototype], to: Request.prototype, method: (type, name, original) => { // Return an anonymous function which calls the original function with Request scope return function () { // Call the original function with the Request scope return original.apply(this, arguments); }; }, }); // Inherit the stream.Readable prototype and lazy initialize the stream on first call to inherited methods inherit_prototype({ from: stream.Readable.prototype, to: Request.prototype, override: (name) => '_super_' + name, // Prefix all overrides with _super_ method: (type, name, original) => { // Initialize a pass through method const passthrough = function () { // Determine if the underlying readable stream has not been initialized yet if (this._readable === null) { // Initialize the readable stream with the route's streaming configuration this._readable = new stream.Readable(this.route.streaming.readable); // Trigger the readable stream initialization event this._body_parser_stream_init(); } // Return the original function with the readable stream as the context return original.apply(this._readable, arguments); }; return passthrough; }, }); module.exports = Request;