ufiber
Version:
Next-gen webserver for node-js developer
309 lines (307 loc) • 8.53 kB
JavaScript
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
const require_consts = require('../consts.cjs');
const require_url = require('../utils/url.cjs');
const require_query = require('../utils/query.cjs');
const require_readable = require('./readable.cjs');
const require_body = require('../utils/body.cjs');
let node_querystring = require("node:querystring");
node_querystring = require_rolldown_runtime.__toESM(node_querystring);
//#region src/http/request.ts
const tryDecodeURIComponent = (str) => require_url.tryDecode(str, decodeURIComponent);
const discardedDuplicates = [
"age",
"authorization",
"content-length",
"content-type",
"etag",
"expires",
"from",
"host",
"if-modified-since",
"if-unmodified-since",
"last-modified",
"location",
"max-forwards",
"proxy-authorization",
"referer",
"retry-after",
"server",
"user-agent"
];
var Request = class {
req;
res;
/**
* The URL pathname (without host or query).
*
* Always begins with `/`.
*
* @example
* "/users/15"
*/
path;
/**
* HTTP method in uppercase (e.g. `POST`, `GET`)
*/
method;
/**
* The raw query string including the leading `?`, or empty string if none.
*
* @example
* "?page=2&limit=10"
* ""
*/
urlQuery;
isSSL;
[require_consts.kCtxReq] = {
body: Object.create(null),
headers: Object.create(null),
routeIndex: 0
};
[require_consts.kMatch];
#stream;
#rawHeader = [];
constructor({ req, res, bodyLimit, methods, isSSL }) {
this.req = req;
this.res = res;
this.method = req.getCaseSensitiveMethod();
const q = req.getQuery();
this.path = req.getUrl();
this.urlQuery = q ? "?" + q : "";
this.isSSL = isSSL;
req.forEach((key, value) => {
this.#rawHeader.push([key, value]);
});
if ([
"POST",
"PUT",
"PATCH"
].includes(this.method) || methods && methods.includes(this.method)) this.#stream = new require_readable.UwsReadable(res, bodyLimit);
}
destroy() {
this[require_consts.kCtxReq].body = Object.create(null);
this.#stream?.destroy(/* @__PURE__ */ new Error("Request cancelled during body read"));
}
/**
* The full request URL including protocol, host, pathname, and query.
*
* @example
* "http://localhost:3000/users/15?active=true"
*/
get url() {
const host = this.getHeader("Host");
return (this.isSSL ? "https://" : "http://") + host + this.path + this.urlQuery;
}
query(key) {
return require_query.getQuery(this.url, key);
}
queries(key) {
return require_query.getQuery(this.url, key, true);
}
/**
* Returns the value of a named route parameter.
*
* @example
* ```ts
* ctx.param('id'); // "123"
* ```
*/
param(field) {
const paramKey = this[require_consts.kMatch][0][this[require_consts.kCtxReq].routeIndex][1][field];
const param = this.#getParamValue(paramKey);
return param && /%/.test(param) ? tryDecodeURIComponent(param) : param;
}
/**
* Returns an object containing all route parameters for the current route.
*
* @example
* ```ts
* ctx.params(); // { id: "123", name: "John" }
* ```
*/
params() {
const decoded = {};
const keys = Object.keys(this[require_consts.kMatch][0][this[require_consts.kCtxReq].routeIndex][1]);
for (const key of keys) {
const value = this.#getParamValue(this[require_consts.kMatch][0][this[require_consts.kCtxReq].routeIndex][1][key]);
if (value !== void 0) decoded[key] = /%/.test(value) ? tryDecodeURIComponent(value) : value;
}
return decoded;
}
/**
* Resolves the parameter value from the match result.
*/
#getParamValue = (paramKey) => this[require_consts.kMatch][1] ? this[require_consts.kMatch][1][paramKey] : paramKey;
/**
* Lazily normalizes and caches all request headers.
*/
#buildHeader() {
const store = this[require_consts.kCtxReq].headers;
if (Object.keys(store).length > 0) return;
for (const [keyRaw, value] of this.#rawHeader) {
const key = keyRaw.toLowerCase();
if (store[key]) {
if (discardedDuplicates.includes(key)) continue;
if (key === "cookie") {
store[key] += "; " + value;
continue;
}
if (key === "set-cookie") {
store[key].push(value);
continue;
}
store[key] += ", " + value;
continue;
}
if (key === "set-cookie") store[key] = [value];
else store[key] = value;
}
}
/**
* Returns raw header pairs in their original order.
*/
get rawHeaders() {
const arr = [];
for (const [k, v] of this.#rawHeader) arr.push(k, v);
return arr;
}
getHeader(field) {
this.#buildHeader();
const headers = this[require_consts.kCtxReq].headers;
if (!field) return headers;
const key = field.toLowerCase();
if (key === "referrer" || key === "referer") return headers["referrer"] || headers["referer"];
return headers[key];
}
/**
* Returns a readable stream of the request body.
*
* @example
* ```ts
* const file = fs.createWriteStream('upload.bin');
* ctx.stream.pipe(file);
* ```
*/
get stream() {
if (this.#stream && !this.#stream.destroyed) return this.#stream;
throw new Error(`Cannot access request body stream for HTTP method '${this.method}'.`);
}
/**
* Reads and returns the request body as a UTF-8 string.
*
* @example
* ```ts
* const text = await ctx.textParse();
* console.log('Body:', text);
* ```
*/
async textParse() {
const body = this[require_consts.kCtxReq].body;
if (body.text) return body.text;
const text = (await this.stream.getBuffer()).toString("utf-8").trim();
body.text = text;
return text;
}
/**
* Reads and returns the request body as an ArrayBuffer.
*
* @example
* ```ts
* const arrayBuffer = await ctx.arrayBuffer();
* console.log(arrayBuffer.byteLength);
* ```
*/
async arrayBuffer() {
const body = this[require_consts.kCtxReq].body;
if (body.arrayBuffer) return body.arrayBuffer;
const buffer = await this.stream.getBuffer();
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
body.arrayBuffer = arrayBuffer;
return arrayBuffer;
}
/**
* Reads the body as a Blob (Node 18+).
*
* @example
* ```ts
* const blob = await ctx.blobParse();
* console.log('Blob size:', blob.size);
* ```
*
* @returns {Promise<Blob>}
*/
async blobParse() {
const body = this[require_consts.kCtxReq].body;
if (body.blob) return body.blob;
const type = this.getHeader("Content-Type") || "application/octet-stream";
const arrayBuffer = await this.arrayBuffer();
const blob = new Blob([arrayBuffer], { type });
body.blob = blob;
return blob;
}
/**
* Parses and returns the request body as JSON.
*
* @template T
* @returns {Promise<T>} The parsed JSON body.
* @throws {SyntaxError} If body is empty or malformed.
*
* @example
* ```ts
* const data = await ctx.jsonParse();
* console.log(data.user, data.email);
* ```
*/
async jsonParse() {
const body = this[require_consts.kCtxReq].body;
if (body.json) return body.json;
const text = await this.textParse();
if (!text) throw new SyntaxError("Empty request body, expected JSON");
try {
const json = JSON.parse(text);
body.json = json;
return json;
} catch (err) {
throw new SyntaxError(`Invalid JSON body: ${err.message}`);
}
}
/**
* Parses form submissions (URL-encoded or multipart/form-data).
*
* @param {FormOption} [options] - Optional multipart parser settings.
* @returns {Promise<FormData>}
* @throws {TypeError} If content type is unsupported.
*
* @example
* ```ts
* const form = await ctx.formParse();
* console.log(form.get('username'));
* ```
*/
async formParse(options) {
const cType = (this.getHeader("content-type") || "").toLowerCase();
const body = this[require_consts.kCtxReq].body;
if (body.formData) return body.formData;
if (cType.startsWith("application/x-www-form-urlencoded")) {
const form = new require_body.FormData();
const text = await this.textParse();
if (!text) throw new SyntaxError("Empty form data");
try {
const parsed = node_querystring.default.parse(text);
for (const [k, v] of Object.entries(parsed)) if (Array.isArray(v)) for (const item of v) form.append(k, item);
else form.append(k, v);
body.formData = form;
return form;
} catch (error) {
throw new SyntaxError("Malformed URL-encoded data");
}
} else if (cType.startsWith("multipart/form-data")) {
const form = require_body.formParse(await this.stream.getBuffer(), cType, options);
body.formData = form;
return form;
}
throw new TypeError(`Content-Type '${cType}' not supported for form parsing`);
}
};
//#endregion
exports.Request = Request;