@adonisjs/bodyparser
Version:
BodyParser middleware for AdonisJS http server to read and parse request body
1,171 lines (1,170 loc) • 35.2 kB
JavaScript
import { a as supportMagicFileTypes, i as getFileType, n as computeFileTypeFromName, r as formBodyNormalizers, t as MultipartFile } from "./file-CX_Xk_Sw.js";
import { extname, isAbsolute, join } from "node:path";
import string from "@poppinss/utils/string";
import { Exception, RuntimeException } from "@poppinss/utils/exception";
import { unlink } from "node:fs/promises";
import { tmpdir } from "node:os";
import { debuglog } from "node:util";
import multiparty from "@poppinss/multiparty";
import lodash from "@poppinss/utils/lodash";
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import inflate from "inflation";
import raw from "raw-body";
import { safeParse } from "@poppinss/utils/json";
import qs from "@poppinss/qs";
import { HttpRequest } from "@adonisjs/http-server";
//#region src/debug.ts
/**
* Debug logger instance for the bodyparser package. Use the DEBUG=adonisjs:bodyparser
* environment variable to enable debug logging.
*
* @example
* ```sh
* DEBUG=adonisjs:bodyparser node ace serve
* ```
*/
var debug_default = debuglog("adonisjs:bodyparser");
//#endregion
//#region src/form_fields.ts
const PROTOTYPE_POLLUTING_KEYS = new Set([
"__proto__",
"prototype",
"constructor"
]);
/**
* A collection of form fields that stores form data while handling
* arrays gracefully
*/
var FormFields = class {
/**
* Internal storage for form fields
*/
#fields = Object.create(null);
#normalizer;
/**
* Creates a new FormFields instance for collecting form data.
*
* @param normalizer - Optional normalizer function to process string values
*/
constructor(normalizer) {
this.#normalizer = normalizer;
}
/**
* Add a new key/value pair. The keys with array-like
* expressions are handled properly.
*
* @param key - The field name, can include array notation
* @param value - The field value
*
* @example
* ```
* formfields.add('username', 'virk')
*
* // array
* formfields.add('username[]', 'virk')
* formfields.add('username[]', 'nikk')
*
* // Indexed keys are ordered properly
* formfields.add('username[1]', 'virk')
* formfields.add('username[0]', 'nikk')
* ```
*/
add(key, value) {
let isArray = false;
/**
* Convert empty strings to null
*/
if (this.#normalizer && typeof value === "string") value = this.#normalizer(value);
/**
* Drop `[]` without indexes, since lodash `_.set` and
* `_.get` methods needs the index or plain key.
*/
key = key.replace(/\[]$/, () => {
isArray = true;
return "";
});
const keyPath = lodash.toPath(key);
if (keyPath.some((segment) => PROTOTYPE_POLLUTING_KEYS.has(segment))) return;
/**
* Check to see if value exists or set it (if missing)
*/
const existingValue = lodash.get(this.#fields, keyPath);
if (!existingValue) {
lodash.set(this.#fields, keyPath, isArray ? [value] : value);
return;
}
/**
* Mutate existing value if it's an array
*/
if (Array.isArray(existingValue)) {
existingValue.push(value);
return;
}
/**
* Set new value + existing value
*/
lodash.set(this.#fields, keyPath, [existingValue, value]);
}
/**
* Returns the collected form fields as an object.
*
* @example
* ```ts
* const fields = new FormFields()
* fields.add('username', 'virk')
* fields.add('tags[]', 'node')
* fields.add('tags[]', 'typescript')
*
* console.log(fields.get())
* // { username: 'virk', tags: ['node', 'typescript'] }
* ```
*/
get() {
return this.#fields;
}
};
//#endregion
//#region src/multipart/part_handler.ts
/**
* Part handler handles the progress of a stream and also internally validates
* its size and extension.
*
* This class offloads the task of validating a file stream, regardless of how
* the stream is consumed. For example:
*
* In classic scenario, we will process the file stream and write files to the
* tmp directory and in more advanced cases, the end user can handle the
* stream by themselves and report each chunk to this class.
*/
var PartHandler = class {
/**
* The multipart stream part being handled
*/
#part;
/**
* Configuration options for file validation and processing
*/
#options;
/**
* The stream buffer reported by the stream consumer. We hold the buffer until are
* able to detect the file extension and then buff memory is released
*/
#buff;
/**
* A boolean to know, if we have emitted the error event after one or
* more validation errors. We need this flag, since the race conditions
* between `data` and `error` events will trigger multiple `error`
* emit.
*/
#emittedValidationError = false;
/**
* A boolean to know if we can use the magic number to detect the file type. This is how it
* works.
*
* - We begin by extracting the file extension from the file name
* - If the file has no extension, we try to inspect the buffer
* - If the extension is something we support via magic numbers, then we ignore the extension
* and inspect the buffer
* - Otherwise, we have no other option than to trust the extension
*
* Think of this as using the optimal way for validating the file type
*/
get #canFileTypeBeDetected() {
const fileExtension = extname(this.#part.filename).replace(/^\./, "");
return fileExtension ? supportMagicFileTypes.has(fileExtension) : true;
}
/**
* Creating a new file object for each part inside the multipart
* form data
*/
file;
/**
* Creates a new part handler instance for processing multipart stream parts
*
* @param part - The multipart stream to handle
* @param options - Validation options and configuration
*/
constructor(part, options) {
this.#part = part;
this.#options = options;
this.file = new MultipartFile({
clientName: part.filename,
fieldName: part.name,
headers: part.headers
}, {
size: options.size,
extnames: options.extnames
});
}
/**
* Detects the file type and extension and also validates it when validations
* are not deferred.
*/
async #detectFileTypeAndExtension() {
if (!this.#buff) return;
let fileType = this.#canFileTypeBeDetected ? await getFileType(this.#buff) : computeFileTypeFromName(this.file.clientName, this.file.headers);
/**
* If magic number detection failed after enough bytes have been
* buffered, fall back to detecting the file type from the
* filename and headers to prevent unbounded memory growth.
*/
if (!fileType && this.#buff.length >= 4100) fileType = computeFileTypeFromName(this.file.clientName, this.file.headers);
if (fileType) {
this.file.extname = fileType.ext;
this.file.type = fileType.type;
this.file.subtype = fileType.subtype;
}
}
/**
* Skip the stream or end it forcefully. This is invoked when the
* streaming consumer reports an error
*/
#skipEndStream() {
this.#part.emit("close");
}
/**
* Finish the process of listening for any more events and mark the
* file state as consumed.
*/
#finish() {
this.file.state = "consumed";
if (!this.#options.deferValidations) this.file.validate();
}
/**
* Marks the file as being in streaming mode. Should be called before
* processing the stream.
*/
begin() {
this.file.state = "streaming";
}
/**
* Handles the file upload progress by validating the file size and
* extension.
*
* @param line - Buffer chunk from the stream
* @param bufferLength - Length of the buffer chunk in bytes
*/
async reportProgress(line, bufferLength) {
/**
* Do not consume stream data when file state is not `streaming`. Stream
* events race conditions may emit the `data` event after the `error`
* event in some cases, so we have to restrict it here.
*/
if (this.file.state !== "streaming") return;
/**
* Detect the file type and extension when extname is null, otherwise
* empty out the buffer. We only need the buffer to find the
* file extension from it's content.
*/
if (this.file.extname === void 0) {
this.#buff = this.#buff ? Buffer.concat([this.#buff, line]) : line;
await this.#detectFileTypeAndExtension();
} else this.#buff = void 0;
/**
* The length of stream buffer
*/
this.file.size = this.file.size + bufferLength;
/**
* Validate the file on every chunk, unless validations have been deferred.
*/
if (this.#options.deferValidations) return;
/**
* Attempt to validate the file after every chunk and report error
* when it has one or more failures. After this the consumer must
* call `reportError`.
*/
this.file.validate();
if (!this.file.isValid && !this.#emittedValidationError) {
this.#emittedValidationError = true;
this.#part.emit("error", new Exception("one or more validations failed", {
code: "E_STREAM_VALIDATION_FAILURE",
status: 400
}));
}
}
/**
* Report errors encountered while processing the stream. These can be errors
* apart from the one reported by this class. For example: The `s3` failure
* due to some bad credentials.
*
* @param error - The error encountered during stream processing
*/
async reportError(error) {
if (this.file.state !== "streaming") return;
this.#skipEndStream();
this.#finish();
if (error.code === "E_STREAM_VALIDATION_FAILURE") return;
/**
* Push to the array of file errors
*/
this.file.errors.push({
fieldName: this.file.fieldName,
clientName: this.file.clientName,
type: "fatal",
message: error.message
});
}
/**
* Report success data about the file.
*
* @param data - Success data containing file paths and metadata
*/
async reportSuccess(data) {
if (this.file.state !== "streaming") return;
/**
* Re-attempt to detect the file extension after we are done
* consuming the stream
*/
if (this.file.extname === void 0) await this.#detectFileTypeAndExtension();
if (data) {
const { filePath, tmpPath, ...meta } = data;
if (filePath) this.file.filePath = filePath;
if (tmpPath) this.file.tmpPath = tmpPath;
this.file.meta = meta || {};
}
this.#finish();
}
};
//#endregion
//#region src/multipart/main.ts
/**
* Multipart class offers a low level API to interact with the incoming
* HTTP request data as a stream. This makes it super easy to
* write files to S3 without saving them to the disk first.
*/
var Multipart = class {
/**
* The HTTP context for the current request
*/
#ctx;
/**
* Configuration options for multipart processing
*/
#config;
/**
* The registered handlers to handle the file uploads
*/
#handlers = {};
/**
* Collected fields from the multipart stream
*/
#fields;
/**
* Collected files from the multipart stream. Files are only collected
* when there is an attached listener for a given file.
*/
#files;
/**
* We track the finishing of `this.onFile` async handlers
* to make sure that `process` promise resolves for all
* handlers to finish.
*/
#pendingHandlers = 0;
/**
* The reference to underlying multiparty form
*/
#form;
/**
* Total size limit of the multipart stream. If it goes beyond
* the limit, then an exception will be raised.
*/
#upperLimit;
/**
* A track of total number of file bytes processed so far
*/
#processedBytes = 0;
/**
* The current state of the multipart form handler
*/
state = "idle";
/**
* Creates a new Multipart instance for processing multipart form data
*
* @param ctx - The HTTP context
* @param config - Configuration options for multipart processing
* @param _featureFlags - Feature flags (unused)
*/
constructor(ctx, config = {}, _featureFlags = {}) {
this.#ctx = ctx;
this.#config = config;
this.#fields = new FormFields(config.normalizer);
this.#files = new FormFields(config.normalizer);
this.#upperLimit = config.limit;
}
/**
* Returns a boolean telling whether all streams have been
* consumed along with all handlers execution
*/
#isClosed() {
return this.#form["flushing"] <= 0 && this.#pendingHandlers <= 0;
}
/**
* Removes array like expression from the part name to
* find the handler
*/
#getHandlerName(name) {
return name.replace(/\[\d*\]/, "");
}
/**
* Validates and returns an error when upper limit is defined and
* processed bytes is over the upper limit
*/
#validateProcessedBytes(chunkLength) {
if (!this.#upperLimit) return;
this.#processedBytes += chunkLength;
if (this.#processedBytes > this.#upperLimit) return new Exception("request entity too large", {
code: "E_REQUEST_ENTITY_TOO_LARGE",
status: 413
});
}
/**
* Handles a given part by invoking it's handler or
* by resuming the part, if there is no defined
* handler
*/
async #handlePart(part) {
/**
* Skip parts with empty name or empty filenames. The empty
* filenames takes place when user doesn't upload a file
* and empty name is more of a bad client scanerio.
*/
if (!part.name || !part.filename) {
part.resume();
return;
}
const name = this.#getHandlerName(part.name);
/**
* Skip, if their is no handler to consume the part.
*/
const handler = this.#handlers[name] || this.#handlers["*"];
if (!handler) {
debug_default("skipping multipart part as there are no handlers \"%s\"", name);
part.resume();
return;
}
debug_default("processing multipart part \"%s\"", name);
this.#pendingHandlers++;
/**
* Instantiate the part handler
*/
const partHandler = new PartHandler(part, handler.options);
partHandler.begin();
/**
* Track the file instance created by the part handler. The end user
* must be able to access these files.
*/
this.#files.add(partHandler.file.fieldName, partHandler.file);
this.#fields.add(partHandler.file.fieldName, partHandler.file);
part.file = partHandler.file;
try {
const response = await handler.handler(part, async (line) => {
if (this.state !== "processing") return;
const lineLength = line.length;
/**
* Keeping an eye on total bytes processed so far and shortcircuit
* request when more than expected bytes have been received.
*/
const error = this.#validateProcessedBytes(lineLength);
if (error) {
part.emit("error", error);
this.abort(error);
return;
}
try {
await partHandler.reportProgress(line, lineLength);
} catch (err) {
part.emit("error", err);
this.abort(err);
}
});
/**
* Stream consumed successfully
*/
await partHandler.reportSuccess(response || {});
} catch (error) {
/**
* The stream handler reported an exception
*/
await partHandler.reportError(error);
}
this.#pendingHandlers--;
}
/**
* Record the fields inside multipart contract
*/
#handleField(key, value) {
if (!key) return;
this.#fields.add(key, value);
}
/**
* Mark the process as finished
*/
#finish(newState) {
if (this.state === "idle" || this.state === "processing") {
this.state = newState;
this.#ctx.request["__raw_files"] = this.#files.get();
this.#ctx.request.setInitialBody(this.#fields.get());
}
}
/**
* Attach handler for a given file. To handle all files, you
* can attach a wildcard handler.
*
* @param name - The field name to handle, or '*' for wildcard
* @param options - Validation options and configuration
* @param handler - The handler function to process the file
*
* @example
* ```ts
* multipart.onFile('package', {}, async (stream) => {
* })
*
* multipart.onFile('*', {}, async (stream) => {
* })
* ```
*/
onFile(name, options, handler) {
this.#handlers[name] = {
handler,
options
};
return this;
}
/**
* Aborts the multipart stream processing by emitting an error event.
* This will stop all file processing and reject the process promise.
*
* @param error - The error that caused the abort
*
* @example
* ```ts
* multipart.onFile('*', {}, async (part) => {
* if (part.file.size > MAX_SIZE) {
* multipart.abort(new Error('File too large'))
* }
* })
* ```
*/
abort(error) {
this.#form.emit("error", error);
}
/**
* Processes the multipart request by parsing all file and field streams.
* Must be called after registering all file handlers with onFile.
*
* @param config - Optional configuration overrides for this specific request
*
* @example
* ```ts
* const multipart = ctx.request.multipart
*
* multipart.onFile('avatar', {}, async (part, reportChunk) => {
* await streamFile(part, '/tmp/avatar.jpg', reportChunk)
* })
*
* await multipart.process()
* ```
*/
process(config) {
return new Promise((resolve, reject) => {
if (this.state !== "idle") {
reject(new Exception("multipart stream has already been consumed", { code: "E_RUNTIME_EXCEPTION" }));
return;
}
this.state = "processing";
/**
* Use local upperlimit
*/
if (config && config.limit) this.#upperLimit = string.bytes.parse(config.limit);
this.#form = new multiparty.Form({
maxFields: config?.maxFields ?? this.#config.maxFields,
maxFieldsSize: this.#config.fieldsLimit
});
debug_default("processing multipart body");
/**
* Raise error when form encounters an
* error
*/
this.#form.on("error", (error) => {
this.#finish("error");
process.nextTick(() => {
if (this.#ctx.request.request.readable) this.#ctx.request.request.resume();
if (error.message.match(/stream ended unexpectedly/)) reject(new Exception("Invalid multipart request", {
status: 400,
code: "E_INVALID_MULTIPART_REQUEST"
}));
else if (error.message.match(/maxFields [0-9]+ exceeded/)) reject(new Exception("Fields length limit exceeded", {
status: 413,
code: "E_REQUEST_ENTITY_TOO_LARGE"
}));
else if (error.message.match(/maxFieldsSize [0-9]+ exceeded/)) reject(new Exception("Fields size in bytes exceeded", {
status: 413,
code: "E_REQUEST_ENTITY_TOO_LARGE"
}));
else reject(error);
});
});
/**
* Process each part at a time and also resolve the
* promise when all parts are consumed and processed
* by their handlers
*/
this.#form.on("part", async (part) => {
await this.#handlePart(part);
/**
* When a stream finishes before the handler, the close `event`
* will not resolve the current Promise. So in that case, we
* check and resolve from here
*/
if (this.#isClosed()) {
this.#finish("success");
resolve();
}
});
/**
* Listen for fields
*/
this.#form.on("field", (key, value) => {
try {
this.#handleField(key, value);
} catch (error) {
this.abort(error);
}
});
/**
* Resolve promise on close, when all internal
* file handlers are done processing files
*/
this.#form.on("close", () => {
if (this.#isClosed()) {
this.#finish("success");
resolve();
}
});
this.#form.parse(this.#ctx.request.request);
});
}
};
//#endregion
//#region src/multipart/stream_file.ts
/**
* Streams a file from a readable stream to a file system location. Automatically
* cleans up on errors and optionally reports data chunks to a listener.
*
* @param readStream - The source readable stream
* @param location - The destination file path
* @param dataListener - Optional callback to receive data chunks
*
* @example
* ```ts
* await streamFile(part, '/tmp/upload.jpg', (chunk) => {
* console.log('Received', chunk.length, 'bytes')
* })
* ```
*/
async function streamFile(readStream, location, dataListener) {
if (typeof dataListener === "function") {
readStream.pause();
readStream.on("data", dataListener);
}
const writeStream = createWriteStream(location);
try {
await pipeline(readStream, writeStream);
} catch (error) {
unlink(writeStream.path).catch(() => {});
throw error;
}
}
//#endregion
//#region src/parsers/text.ts
/**
* Prepares parser options for raw text body parsing by applying defaults
* for encoding and size limits.
*
* @param options - Raw body parser configuration
*/
function prepareTextParserOptions(options) {
return {
encoding: options.encoding ?? "utf8",
limit: options.limit ?? "56kb"
};
}
/**
* Parses and inflates the raw request body as text. Automatically handles
* compressed request bodies (gzip, deflate, etc.).
*
* @param req - The incoming HTTP request
* @param options - Parser options including encoding and size limits
*/
function parseText(req, options) {
/**
* Mimicing behavior of
* https://github.com/poppinss/co-body/blob/master/lib/text.js#L30
*/
const contentLength = req.headers["content-length"];
const encoding = req.headers["content-encoding"] || "identity";
if (contentLength && encoding === "identity") options = {
...options,
length: ~~contentLength
};
return raw(inflate(req), options);
}
//#endregion
//#region src/parsers/json.ts
/**
* Allowed whitespace is defined in RFC 7159
* http://www.rfc-editor.org/rfc/rfc7159.txt
*/
const strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
/**
* Prepares parser options for JSON body parsing by configuring strict mode
* and value normalization through a reviver function.
*
* @param options - JSON body parser configuration
*/
function prepareJSONParserOptions(options) {
let normalizer;
if (options.convertEmptyStringsToNull && options.trimWhitespaces) normalizer = formBodyNormalizers.trimWhitespacesAndConvertToNull;
else if (options.convertEmptyStringsToNull) normalizer = formBodyNormalizers.convertToNull;
else if (options.trimWhitespaces) normalizer = formBodyNormalizers.trimWhitespaces;
return {
...prepareTextParserOptions(options),
strict: options.strict !== false,
reviver: normalizer ? function JSONReviver(key, value) {
if (key === "") return value;
return typeof value === "string" ? normalizer(value) : value;
} : void 0
};
}
/**
* Parses JSON request body with optional strict mode that enforces only
* objects and arrays as valid JSON. Returns both parsed and raw representations.
*
* @param req - The incoming HTTP request
* @param options - Parser options including encoding, limits, and strict mode
*
* @example
* ```ts
* const { parsed, raw } = await parseJSON(request, options)
* // parsed: { username: 'virk', age: 28 }
* // raw: '{"username":"virk","age":28}'
* ```
*/
async function parseJSON(req, options) {
const requestBody = await parseText(req, options);
/**
* Do not parse body when request body is empty
*/
if (!requestBody) return options.strict ? {
parsed: {},
raw: requestBody
} : {
parsed: requestBody,
raw: requestBody
};
/**
* Test JSON body to ensure it is valid JSON in strict mode
*/
if (options.strict && !strictJSONReg.test(requestBody)) throw new Exception("Invalid JSON, only supports object and array", { status: 422 });
try {
return {
parsed: safeParse(requestBody, options.reviver),
raw: requestBody
};
} catch (error) {
error.status = 400;
error.body = requestBody;
throw error;
}
}
//#endregion
//#region src/parsers/form.ts
/**
* Prepares parser options for URL-encoded form data by configuring
* query string parsing and value normalization.
*
* @param options - Form body parser configuration
*/
function prepareFormParserOptions(options) {
/**
* Shallow clone query string options
*/
const queryStringOptions = { ...options.queryString };
if (queryStringOptions.allowDots === void 0) queryStringOptions.allowDots = true;
let normalizer;
if (options.convertEmptyStringsToNull && options.trimWhitespaces) normalizer = formBodyNormalizers.trimWhitespacesAndConvertToNull;
else if (options.convertEmptyStringsToNull) normalizer = formBodyNormalizers.convertToNull;
else if (options.trimWhitespaces) normalizer = formBodyNormalizers.trimWhitespaces;
/**
* Convert empty strings to null
*/
if (normalizer) queryStringOptions.decoder = function(str, defaultDecoder, charset, type) {
let value = defaultDecoder(str, defaultDecoder, charset);
if (type === "value") return normalizer(value);
return value;
};
/**
* Shallow clone of provided options
*/
return {
...prepareTextParserOptions(options),
qs: queryStringOptions
};
}
/**
* Parses URL-encoded form data (application/x-www-form-urlencoded) from
* the request body and returns both parsed and raw representations.
*
* @param req - The incoming HTTP request
* @param options - Parser options including encoding, limits, and query string config
*
* @example
* ```ts
* const { parsed, raw } = await parseForm(request, options)
* // parsed: { username: 'virk', tags: ['node', 'typescript'] }
* // raw: 'username=virk&tags=node&tags=typescript'
* ```
*/
async function parseForm(req, options) {
const requestBody = await parseText(req, options);
return {
parsed: qs.parse(requestBody, options.qs),
raw: requestBody
};
}
//#endregion
//#region src/bindings/request.ts
/**
* Updates the validation options on a file instance if they haven't been set already.
*
* @param file - The multipart file instance to update
* @param options - Validation options including size limit and allowed extensions
*/
function setFileOptions(file, options) {
if (file.sizeLimit === void 0 && options && options.size) file.sizeLimit = options.size;
if (file.allowedExtensions === void 0 && options && options.extnames) file.allowedExtensions = options.extnames;
}
/**
* Type guard to check if a value is an instance of MultipartFile.
*
* @param file - The value to check
*/
function isInstanceOfFile(file) {
return file && file instanceof MultipartFile;
}
debug_default("extending request class with \"file\", \"files\" and \"allFiles\" macros");
/**
* Extends the Request class toJSON method to serialize files alongside
* the rest of the request data.
*/
HttpRequest.macro("toJSON", function() {
return {
...this.serialize(),
files: this["__raw_files"] || {}
};
});
/**
* Extends the Request class with a method to fetch a single uploaded file.
* When an array of files exists for the key, returns the first file.
*
* @example
* ```ts
* const avatar = request.file('avatar', {
* size: '2mb',
* extnames: ['jpg', 'png', 'jpeg']
* })
*
* if (avatar && avatar.isValid) {
* await avatar.move(app.publicPath('uploads'))
* }
* ```
*/
HttpRequest.macro("file", function getFile(key, options) {
let file = lodash.get(this.allFiles(), key);
file = Array.isArray(file) ? file[0] : file;
if (!isInstanceOfFile(file)) return null;
setFileOptions(file, options);
file.validate();
return file;
});
/**
* Extends the Request class with a method to fetch all uploaded files for
* a given field name. Always returns an array, even if a single file was uploaded.
*
* @example
* ```ts
* const documents = request.files('documents', {
* size: '5mb',
* extnames: ['pdf', 'doc', 'docx']
* })
*
* for (const doc of documents) {
* if (doc.isValid) {
* await doc.move(app.publicPath('uploads'))
* }
* }
* ```
*/
HttpRequest.macro("files", function getFiles(key, options) {
let files = lodash.get(this.allFiles(), key);
files = Array.isArray(files) ? files : files ? [files] : [];
return files.filter(isInstanceOfFile).map((file) => {
setFileOptions(file, options);
file.validate();
return file;
});
});
/**
* Extends the Request class with a method to fetch all uploaded files
* from the request. Throws an error if the bodyparser middleware is not registered.
*
* @example
* ```ts
* const allFiles = request.allFiles()
* // { avatar: MultipartFile, documents: [MultipartFile, MultipartFile] }
* ```
*/
HttpRequest.macro("allFiles", function allFiles() {
if (!this.__raw_files) throw new RuntimeException("Cannot read files. Make sure the bodyparser middleware is registered");
return this["__raw_files"];
});
//#endregion
//#region src/parsers/multipart.ts
/**
* Prepares configuration for multipart form data parsing by converting
* size limits from strings to bytes and configuring value normalization.
*
* @param config - Multipart body parser configuration
*/
function prepareMultipartConfig(config) {
let normalizer;
if (config.convertEmptyStringsToNull && config.trimWhitespaces) normalizer = formBodyNormalizers.trimWhitespacesAndConvertToNull;
else if (config.convertEmptyStringsToNull) normalizer = formBodyNormalizers.convertToNull;
else if (config.trimWhitespaces) normalizer = formBodyNormalizers.trimWhitespaces;
return {
limit: config.limit ? string.bytes.parse(config.limit) : void 0,
fieldsLimit: config.fieldsLimit ? string.bytes.parse(config.fieldsLimit) : void 0,
maxFields: config.maxFields,
normalizer
};
}
//#endregion
//#region src/bodyparser_middleware.ts
/**
* Bindings to extend request
*/
/**
* BodyParser middleware parses the incoming request body and sets it as
* request body to be read later in the request lifecycle.
*/
var BodyParserMiddleware = class {
/**
* Body parser configuration
*/
#config;
#parsersConfig;
/**
* Creates a new BodyParserMiddleware instance
*
* @param config - The body parser configuration
* @param _featureFlags - Feature flags (unused)
*/
constructor(config, _featureFlags) {
this.#config = config;
this.#parsersConfig = {
raw: prepareTextParserOptions(this.#config.raw),
form: prepareFormParserOptions(this.#config.form),
json: prepareJSONParserOptions(this.#config.json),
multipart: prepareMultipartConfig(this.#config.multipart)
};
debug_default("using config %O", this.#config);
}
/**
* Checks if the request content-type header matches any of the expected types.
*
* @param request - The HTTP request object
* @param types - Array of MIME types to check against
*/
#isType(request, types) {
return !!(types && types.length && request.is(types));
}
/**
* Converts raw-body errors into AdonisJS exceptions with appropriate
* status codes and error codes.
*
* @param error - The raw-body error object
*/
#getExceptionFor(error) {
switch (error.type) {
case "encoding.unsupported": return new Exception(error.message, {
status: error.status,
code: "E_ENCODING_UNSUPPORTED"
});
case "entity.too.large": return new Exception(error.message, {
status: error.status,
code: "E_REQUEST_ENTITY_TOO_LARGE"
});
case "request.aborted": return new Exception(error.message, {
status: error.status,
code: "E_REQUEST_ABORTED"
});
default: return error;
}
}
/**
* Generates a temporary file path for storing uploaded files. Uses the
* configured tmpFileName function if provided, otherwise generates a UUID.
*
* @param config - The multipart configuration
*/
#getTmpPath(config) {
if (typeof config.tmpFileName === "function") {
const tmpPath = config.tmpFileName();
return isAbsolute(tmpPath) ? tmpPath : join(tmpdir(), tmpPath);
}
return join(tmpdir(), string.uuid());
}
/**
* Handle HTTP request body by parsing it according to the user
* configuration
*
* @param ctx - The HTTP context
* @param next - The next middleware function
*/
async handle(ctx, next) {
/**
* Initiating the `__raw_files` property as an object
*/
ctx.request["__raw_files"] = {};
const requestUrl = ctx.request.url();
const requestMethod = ctx.request.method();
/**
* Only process for whitelisted nodes
*/
if (!this.#config.allowedMethods.includes(requestMethod)) {
debug_default("skipping HTTP request \"%s:%s\"", requestMethod, requestUrl);
return next();
}
/**
* Return early when request body is empty. Many clients set the `Content-length = 0`
* when request doesn't have any body, which is not handled by the below method.
*
* The main point of `hasBody` is to early return requests with empty body created by
* clients with missing headers.
*/
if (!ctx.request.hasBody()) {
debug_default("skipping as request has no body \"%s:%s\"", requestMethod, requestUrl);
return next();
}
/**
* Handle multipart form
*/
const multipartConfig = this.#config["multipart"];
if (this.#isType(ctx.request, multipartConfig.types)) {
debug_default("detected multipart request \"%s:%s\"", requestMethod, requestUrl);
ctx.request.multipart = new Multipart(ctx, this.#parsersConfig.multipart, {});
ctx.request.bodyType = "multipart";
/**
* Skip parsing when `autoProcess` is disabled
*/
if (multipartConfig.autoProcess === false) {
debug_default("skipping auto processing of multipart request \"%s:%s\"", requestMethod, requestUrl);
return next();
}
/**
* Skip parsing when the current route matches one of the defined
* processManually route patterns.
*/
if (ctx.route && multipartConfig.processManually.includes(ctx.route.pattern)) {
debug_default("skipping auto processing of multipart request \"%s:%s\"", requestMethod, requestUrl);
return next();
}
/**
* Skip parsing when the current route matches one of the "autoProcess"
* patterns
*/
if (ctx.route && Array.isArray(multipartConfig.autoProcess) && !multipartConfig.autoProcess.includes(ctx.route.pattern)) {
debug_default("skipping auto processing of multipart request \"%s:%s\"", requestMethod, requestUrl);
return next();
}
/**
* Make sure we are not running any validations on the uploaded files. They are
* deferred for the end user when they will access file using `request.file`
* method.
*/
debug_default("auto processing multipart request \"%s:%s\"", requestMethod, requestUrl);
ctx.request.multipart.onFile("*", { deferValidations: true }, async (part, reporter) => {
/**
* We need to abort the main request when we are unable to process any
* file. Otherwise the error will endup on the file object, which
* is incorrect.
*/
try {
const tmpPath = this.#getTmpPath(multipartConfig);
await streamFile(part, tmpPath, reporter);
return { tmpPath };
} catch (error) {
ctx.request.multipart.abort(error);
}
});
try {
await ctx.request.multipart.process();
return next();
} catch (error) {
throw error;
}
}
/**
* Handle url-encoded form data
*/
const formConfig = this.#config["form"];
if (this.#isType(ctx.request, formConfig.types)) {
debug_default("detected urlencoded request \"%s:%s\"", requestMethod, requestUrl);
try {
const { parsed, raw } = await parseForm(ctx.request.request, this.#parsersConfig.form);
ctx.request.setInitialBody(parsed);
ctx.request.updateRawBody(raw);
ctx.request.bodyType = "urlencoded";
return next();
} catch (error) {
throw this.#getExceptionFor(error);
}
}
/**
* Handle content with JSON types
*/
const jsonConfig = this.#config["json"];
if (this.#isType(ctx.request, jsonConfig.types)) {
debug_default("detected JSON request \"%s:%s\"", requestMethod, requestUrl);
try {
const { parsed, raw } = await parseJSON(ctx.request.request, this.#parsersConfig.json);
ctx.request.setInitialBody(parsed);
ctx.request.updateRawBody(raw);
ctx.request.bodyType = "json";
return next();
} catch (error) {
throw this.#getExceptionFor(error);
}
}
/**
* Handles raw request body
*/
const rawConfig = this.#config["raw"];
if (this.#isType(ctx.request, rawConfig.types)) {
debug_default("parsing raw body \"%s:%s\"", requestMethod, requestUrl);
try {
ctx.request.setInitialBody({});
ctx.request.updateRawBody(await parseText(ctx.request.request, this.#parsersConfig.raw));
ctx.request.bodyType = "raw";
return next();
} catch (error) {
throw this.#getExceptionFor(error);
}
}
ctx.request.bodyType = "unknown";
await next();
}
};
//#endregion
export { BodyParserMiddleware as t };