sensai
Version:
Because even AI needs a master
175 lines (174 loc) • 6.05 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
default: function() {
return _default;
},
normalizeCharset: function() {
return normalizeCharset;
}
});
const _querystring = require("../../utils/querystring");
const _error = /*#__PURE__*/ _interop_require_default(require("./error"));
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
/**
* Parses content type header to extract main type and charset
* @param contentType The content-type header value
* @returns Object containing mainType and charset
*/ function parseContentType(contentType) {
const mainType = contentType.split(";")[0].trim().toLowerCase();
const charsetMatch = contentType.match(/charset=([^;]+)/i);
const charset = charsetMatch ? normalizeCharset(charsetMatch[1].trim()) : "utf-8";
return {
mainType,
charset
};
}
/**
* Normalize HTTP charset to Node.js supported encodings
* @notes we do not suppor windows-1252 as it is not a standard encoding
* @param charset The charset from HTTP headers
* @returns Normalized charset name for Node.js Buffer
*/ function normalizeCharset(charset) {
charset = charset.toLowerCase();
switch(charset){
case "iso-8859-1":
return "latin1";
case "us-ascii":
return "ascii";
default:
return charset;
}
}
/**
* Collects the body data from the request stream with proper limits
* @param request The IncomingMessage request object
* @param contentLength Content length from header
* @param limit Maximum allowed size in bytes (default: 1MB)
* @returns Promise that resolves to the body as Buffer
*/ async function getBodyFromRequest(request, contentLength, limit = 1000000 // 1MB
) {
return new Promise((resolve, reject)=>{
// Check content length immediately if available
if (contentLength && contentLength > limit) {
return reject(new _error.default(413, "Request entity too large"));
}
let complete = false;
let buffer = [];
let received = 0;
// resolve/reject promise and clean up resources
function done(err, value) {
if (complete) return;
complete = true;
// Clean up event listeners
request.removeListener("data", onData);
request.removeListener("end", onEnd);
request.removeListener("error", onError);
request.removeListener("close", onClose);
if (err) {
// Unpipe everything and pause the stream
request.unpipe();
request.pause();
reject(err);
} else {
// Free memory after creating the final buffer
const result = Buffer.concat(buffer);
buffer = [];
resolve(result);
}
}
// Handle data chunks
function onData(chunk) {
received += chunk.length;
// Enforce size limit including current chunk
if (received > limit) {
done(new _error.default(413, "Request entity too large"));
} else {
buffer.push(chunk);
}
}
// Handle stream completion
function onEnd() {
// Validate content length if provided
if (contentLength && received !== contentLength) {
done(new _error.default(400, "Request size did not match content length"));
} else {
done(null, Buffer.concat(buffer));
}
}
// Handle errors
function onError(err) {
done(new _error.default(400, `Request error: ${err.message}`));
}
// Handle premature close
function onClose() {
if (!complete) {
done(new _error.default(499, "Client closed request"));
}
}
// Attach listeners
request.on("data", onData);
request.on("end", onEnd);
request.on("error", onError);
request.on("close", onClose);
});
}
/**
* Parse the request body based on the content-type header.
* Supports JSON, URL-encoded form data, and raw text.
*
* @param request The IncomingMessage request object
* @param limit Maximum allowed size in bytes (default: 1MB)
* @returns Promise that resolves to the parsed body
*/ async function parseBody(request, limit = 1000000 // 1MB
) {
const { headers } = request;
const contentType = headers["content-type"] || "";
// Parse content type once
const { mainType, charset } = parseContentType(contentType);
// Get content length or default to 0 if not present
const contentLength = Number(headers["content-length"]) || 0;
// Collect the body data
const bodyBuffer = await getBodyFromRequest(request, contentLength, limit);
// If body is empty, return empty object
if (!bodyBuffer.length) {
return {};
}
// Convert buffer to string using the specified charset
let body;
try {
body = bodyBuffer.toString(charset);
} catch (err) {
// Fallback to UTF-8 if the charset is not supported
body = bodyBuffer.toString();
}
// Parse based on content type
switch(mainType){
case "application/json":
try {
return JSON.parse(body);
} catch (err) {
throw new _error.default(400, `Invalid JSON: ${err.message}`);
}
case "application/x-www-form-urlencoded":
return (0, _querystring.parse)(body);
default:
// If content type is not recognized, return raw body
return {
raw: body
};
}
}
const _default = parseBody;