bunway
Version:
Express-style routing toolkit built natively for Bun.
127 lines • 4.06 kB
JavaScript
/**
* Error helpers shared across bunWay.
*
* Example usage inside a route:
* ```ts
* app.get("/secret", () => {
* throw new HttpError(403, "Forbidden", {
* headers: { "X-Reason": "AUTH" },
* body: { error: "Forbidden" },
* });
* });
* ```
*
* The router (or errorHandler middleware) catches the error, calls
* {@link buildHttpErrorResponse}, and sends a properly typed response that
* respects the client's `Accept` header.
*/
export class HttpError extends Error {
status;
headers;
body;
constructor(status, message, options = {}) {
const { cause, headers, body } = options;
if (cause !== undefined) {
super(message ?? `HTTP ${status}`, { cause });
}
else {
super(message ?? `HTTP ${status}`);
}
this.name = "HttpError";
this.status = status;
this.headers = headers ? { ...headers } : {};
if (body !== undefined) {
this.body = body;
}
else if (message !== undefined) {
this.body = { error: message };
}
}
}
/** Type guard to detect HttpError instances coming from user code. */
export function isHttpError(value) {
return value instanceof HttpError;
}
function prefersJson(accept) {
if (!accept || accept === "*/*")
return true;
const values = accept.toLowerCase().split(",");
for (const raw of values) {
const media = raw.trim();
if (!media)
continue;
if (media.includes("application/json") || media.includes("+json")) {
return true;
}
if (media.includes("text/plain")) {
return false;
}
}
return accept.includes("*/*");
}
function normalizeBody(error, prefersJsonResponse) {
if (error.body instanceof Response) {
return { body: error.body.body ?? null, contentType: undefined };
}
if (typeof error.body === "string") {
return { body: error.body, contentType: "text/plain" };
}
if (error.body !== undefined) {
if (prefersJsonResponse) {
try {
return {
body: JSON.stringify(error.body),
contentType: "application/json",
};
}
catch {
return {
body: String(error.body),
contentType: "text/plain",
};
}
}
const fallback = typeof error.body === "object" && error.body !== null
? (error.message ?? `HTTP ${error.status}`)
: String(error.body);
return { body: fallback, contentType: "text/plain" };
}
const fallbackMessage = error.message ?? `HTTP ${error.status}`;
if (prefersJsonResponse) {
return {
body: JSON.stringify({ error: fallbackMessage }),
contentType: "application/json",
};
}
return { body: fallbackMessage, contentType: "text/plain" };
}
/**
* Convert an {@link HttpError} into a Bun/Fetch {@link Response}.
* Automatically negotiates JSON vs text and ensures custom headers are applied.
*/
export function buildHttpErrorResponse(ctx, error) {
if (error.body instanceof Response) {
const base = error.body;
const merged = new Headers(base.headers);
for (const [key, value] of Object.entries(error.headers)) {
merged.set(key, value);
}
return new Response(base.body, {
status: error.status,
headers: merged,
});
}
const wantsJson = prefersJson(ctx.req.headers.get("accept"));
const normalized = normalizeBody(error, wantsJson);
const headers = new Headers(error.headers);
const body = normalized.body;
const contentType = normalized.contentType;
if (contentType && !headers.has("Content-Type")) {
headers.set("Content-Type", contentType);
}
return new Response(body, {
status: error.status,
headers,
});
}
//# sourceMappingURL=errors.js.map