A streaming parser for HTML form data for node.js
npx @tessl/cli install tessl/npm-busboy@1.6.0Busboy is a high-performance streaming parser for HTML form data in Node.js applications. It provides efficient parsing of multipart/form-data and application/x-www-form-urlencoded content types with configurable security limits and full event-based API support for handling file uploads and form fields.
npm install busboyconst busboy = require("busboy");For ES modules:
import busboy from "busboy";const http = require("http");
const busboy = require("busboy");
http.createServer((req, res) => {
if (req.method === "POST") {
const bb = busboy({ headers: req.headers });
bb.on("file", (name, file, info) => {
const { filename, encoding, mimeType } = info;
console.log(`File [${name}]: filename: ${filename}, encoding: ${encoding}, mimeType: ${mimeType}`);
file.on("data", (data) => {
console.log(`File [${name}] got ${data.length} bytes`);
}).on("close", () => {
console.log(`File [${name}] done`);
});
});
bb.on("field", (name, val, info) => {
console.log(`Field [${name}]: value: ${val}`);
});
bb.on("close", () => {
console.log("Done parsing form!");
res.writeHead(303, { Connection: "close", Location: "/" });
res.end();
});
req.pipe(bb);
}
}).listen(8000);Busboy is built around a streaming parser architecture:
Creates a streaming parser instance based on the Content-Type header.
/**
* Creates and returns a new Writable form parser stream
* @param {BusboyConfig} config - Configuration object with headers and options
* @returns {Writable} Parser stream (Multipart or URLEncoded)
* @throws {Error} If Content-Type is missing or unsupported
*/
function busboy(config);
interface BusboyConfig {
/** HTTP headers of the incoming request (must include 'content-type') */
headers: { [key: string]: string };
/** highWaterMark for parser stream */
highWaterMark?: number;
/** highWaterMark for individual file streams */
fileHwm?: number;
/** Default character set when not defined (default: 'utf8') */
defCharset?: string;
/** Default charset for multipart parameter values (default: undefined, no decoding) */
defParamCharset?: string;
/** Preserve paths in filenames for multipart (default: false) */
preservePath?: boolean;
/** Various limits on incoming data */
limits?: BusboyLimits;
}
interface BusboyLimits {
/** Max field name size in bytes (default: 100) */
fieldNameSize?: number;
/** Max field value size in bytes (default: 1048576) */
fieldSize?: number;
/** Max number of non-file fields (default: Infinity) */
fields?: number;
/** Max file size in bytes for multipart (default: Infinity) */
fileSize?: number;
/** Max number of file fields for multipart (default: Infinity) */
files?: number;
/** Max number of parts for multipart (default: Infinity) */
parts?: number;
/** Max number of header key-value pairs for multipart (default: 2000) */
headerPairs?: number;
}Emitted for each file found in multipart/form-data requests.
/**
* File event - emitted for each new file found
* @param {string} name - Form field name
* @param {Readable} stream - File data stream with possible 'truncated' property
* @param {FileInfo} info - File metadata
*/
parser.on("file", (name, stream, info) => {});
interface FileInfo {
/** File's filename (WARNING: validate before use) */
filename?: string;
/** File's Content-Transfer-Encoding value */
encoding: string;
/** File's Content-Type value */
mimeType: string;
}Usage Example:
bb.on("file", (name, file, info) => {
const { filename, encoding, mimeType } = info;
// Always consume the stream to prevent hanging
file.on("data", (data) => {
// Process file data chunk
console.log(`Received ${data.length} bytes`);
});
file.on("limit", () => {
console.log("File size limit reached, file truncated");
});
file.on("close", () => {
console.log("File stream closed");
// Check file.truncated property if needed
if (file.truncated) {
console.log("File was truncated due to size limit");
}
});
});Emitted for each non-file field found in form data.
/**
* Field event - emitted for each new non-file field found
* @param {string} name - Form field name
* @param {string} value - Field value
* @param {FieldInfo} info - Field metadata
*/
parser.on("field", (name, value, info) => {});
interface FieldInfo {
/** Whether field name was truncated due to limits */
nameTruncated: boolean;
/** Whether field value was truncated due to limits */
valueTruncated: boolean;
/** Field's Content-Transfer-Encoding value */
encoding: string;
/** Field's Content-Type value */
mimeType: string;
}Usage Example:
bb.on("field", (name, val, info) => {
console.log(`Field [${name}]: ${val}`);
if (info.nameTruncated) {
console.log("Field name was truncated");
}
if (info.valueTruncated) {
console.log("Field value was truncated");
}
});Events emitted when configured limits are reached.
/**
* Parts limit event - emitted when configured parts limit reached
* No more 'file' or 'field' events will be emitted
*/
parser.on("partsLimit", () => {});
/**
* Files limit event - emitted when configured files limit reached
* No more 'file' events will be emitted
*/
parser.on("filesLimit", () => {});
/**
* Fields limit event - emitted when configured fields limit reached
* No more 'field' events will be emitted
*/
parser.on("fieldsLimit", () => {});Usage Example:
const bb = busboy({
headers: req.headers,
limits: {
files: 3,
fields: 10,
parts: 15
}
});
bb.on("filesLimit", () => {
console.log("Maximum number of files reached");
});
bb.on("fieldsLimit", () => {
console.log("Maximum number of fields reached");
});
bb.on("partsLimit", () => {
console.log("Maximum number of parts reached");
});Standard Node.js stream events for completing form processing.
/**
* Close event - emitted when parsing is complete
* All files and fields have been processed
*/
parser.on("close", () => {});
/**
* Finish event - emitted when all data has been written to the parser
* Occurs before close event
*/
parser.on("finish", () => {});
/**
* Error event - emitted when parsing errors occur
* @param {Error} error - Error object with details
*/
parser.on("error", (error) => {});Usage Example:
bb.on("close", () => {
console.log("Form parsing completed successfully");
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Upload complete!");
});
bb.on("error", (err) => {
console.error("Parsing error:", err);
res.writeHead(400, { "Content-Type": "text/plain" });
res.end("Bad request");
});
// Pipe the request to busboy to start parsing
req.pipe(bb);Since busboy returns a Writable stream, it supports standard stream methods.
/**
* Write data to the parser
* @param {Buffer|string} chunk - Data chunk to parse
* @param {string} encoding - String encoding (if chunk is string)
* @param {Function} callback - Callback when write completes
* @returns {boolean} Whether stream can accept more data
*/
parser.write(chunk, encoding, callback);
/**
* Signal end of input data
* @param {Buffer|string} chunk - Optional final chunk
* @param {string} encoding - String encoding (if chunk is string)
* @param {Function} callback - Callback when end completes
*/
parser.end(chunk, encoding, callback);
/**
* Destroy the parser stream
* @param {Error} error - Optional error to emit
*/
parser.destroy(error);Busboy throws or emits errors in the following scenarios:
headers['content-type'] is missing or invalidCommon Error Handling Pattern:
try {
const bb = busboy({ headers: req.headers });
bb.on("error", (err) => {
console.error("Busboy parsing error:", err.message);
if (!res.headersSent) {
res.writeHead(400, { "Content-Type": "text/plain" });
res.end("Invalid form data");
}
});
req.pipe(bb);
} catch (err) {
console.error("Busboy creation error:", err.message);
res.writeHead(400, { "Content-Type": "text/plain" });
res.end("Invalid request");
}const fs = require("fs");
const path = require("path");
const { randomBytes } = require("crypto");
bb.on("file", (name, file, info) => {
// Generate safe filename
const ext = path.extname(info.filename || "");
const filename = randomBytes(16).toString("hex") + ext;
const saveTo = path.join("/uploads", filename);
const writeStream = fs.createWriteStream(saveTo);
file.pipe(writeStream);
writeStream.on("close", () => {
console.log(`File ${name} saved to ${saveTo}`);
});
});const files = {};
const fields = {};
bb.on("file", (name, file, info) => {
const chunks = [];
file.on("data", (data) => {
chunks.push(data);
});
file.on("close", () => {
files[name] = {
buffer: Buffer.concat(chunks),
info: info
};
});
});
bb.on("field", (name, val, info) => {
fields[name] = { value: val, info: info };
});
bb.on("close", () => {
console.log("Collected files:", Object.keys(files));
console.log("Collected fields:", Object.keys(fields));
});const bb = busboy({
headers: req.headers,
limits: {
fieldNameSize: 100, // Limit field name length
fieldSize: 1024 * 1024, // 1MB field value limit
fields: 20, // Max 20 fields
fileSize: 10 * 1024 * 1024, // 10MB file limit
files: 5, // Max 5 files
parts: 25, // Max 25 total parts
headerPairs: 100 // Limit header pairs (multipart only)
}
});