Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 30 additions & 47 deletions src/runtime/internal/error/dev.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { H3Event, HTTPError, HTTPEvent } from "h3";
import { HTTPError, type HTTPEvent } from "h3";
import { getRequestURL } from "h3";
import { readFile } from "node:fs/promises";
import { resolve, dirname } from "node:path";
Expand Down Expand Up @@ -28,20 +28,18 @@ export async function defaultHandler(
event: HTTPEvent,
opts?: { silent?: boolean; json?: boolean }
): Promise<InternalHandlerResponse> {
const isSensitive = error.unhandled;
const status = error.status || 500;
// prettier-ignore
const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true })
const unhandled = error.unhandled ?? !HTTPError.isError(error);
const { status = 500, statusText = "" } = unhandled ? {} : error;
const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true });

// Redirects with base URL
if (status === 404) {
const baseURL = import.meta.baseURL || "/";
if (/^\/[^/]/.test(baseURL) && !url.pathname.startsWith(baseURL)) {
const redirectTo = `${baseURL}${url.pathname.slice(1)}${url.search}`;
return {
status: 302,
statusText: "Found",
headers: { location: redirectTo },
headers: new Headers({ location: `${baseURL}${url.pathname.slice(1)}${url.search}` }),
body: `Redirecting...`,
};
}
Expand All @@ -54,58 +52,43 @@ export async function defaultHandler(
const youch = new Youch();

// Console output
if (isSensitive && !opts?.silent) {
// prettier-ignore
const tags = [error.unhandled && "[unhandled]"].filter(Boolean).join(" ")
const ansiError = await (await youch.toANSI(error)).replaceAll(process.cwd(), ".");
consola.error(`[request error] ${tags} [${event.req.method}] ${url}\n\n`, ansiError);
if (unhandled && !opts?.silent) {
const ansiError = (await youch.toANSI(error)).replaceAll(process.cwd(), ".");
consola.error(`[request error] [${event.req.method}] ${url}\n\n`, ansiError);
}

// Use HTML response only when user-agent expects it (browsers)
const useJSON = opts?.json ?? !event.req.headers.get("accept")?.includes("text/html");

// Prepare headers
const headers: HeadersInit = {
"content-type": useJSON ? "application/json" : "text/html",
// Prevent browser from guessing the MIME types of resources.
"x-content-type-options": "nosniff",
// Prevent error page from being embedded in an iframe
"x-frame-options": "DENY",
// Prevent browsers from sending the Referer header
"referrer-policy": "no-referrer",
// Disable the execution of any js
"content-security-policy":
"script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';",
};

if (status === 404 || !(event as H3Event).res.headers.has("cache-control")) {
headers["cache-control"] = "no-cache";
}
const headers = new Headers(unhandled ? {} : error.headers);

// Prepare body
const body = useJSON
? {
if (useJSON) {
headers.set("Content-Type", "application/json; charset=utf-8");
return {
status,
statusText,
headers,
body: {
error: true,
url,
status,
statusText: error.statusText,
message: error.message,
data: error.data,
stack: error.stack?.split("\n").map((line) => line.trim()),
}
: await youch.toHTML(error, {
request: {
url: url.href,
method: event.req.method,
headers: Object.fromEntries(event.req.headers.entries()),
},
});
...error?.toJSON?.(),
},
};
}

// HTML response
headers.set("Content-Type", "text/html; charset=utf-8");
return {
status,
statusText: error.statusText,
statusText: unhandled ? "" : error.statusText,
headers,
body,
body: await youch.toHTML(error, {
request: {
url: url.href,
method: event.req.method,
headers: Object.fromEntries(event.req.headers.entries()),
},
}),
};
}

Expand Down
56 changes: 13 additions & 43 deletions src/runtime/internal/error/prod.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { H3Event, HTTPError, HTTPEvent } from "h3";
import { HTTPError, type H3Event, type HTTPEvent } from "h3";
import type { InternalHandlerResponse } from "./utils.ts";
import { FastResponse } from "srvx";
import type { NitroErrorHandler } from "nitro/types";
Expand All @@ -13,61 +13,31 @@ const errorHandler: NitroErrorHandler = (error, event) => {

export default errorHandler;

export function defaultHandler(
error: HTTPError,
event: HTTPEvent,
opts?: { silent?: boolean; json?: boolean }
): InternalHandlerResponse {
const isSensitive = error.unhandled;
const status = error.status || 500;
const url = (event as H3Event).url || new URL(event.req.url);
export function defaultHandler(error: HTTPError, event: HTTPEvent): InternalHandlerResponse {
const unhandled = error.unhandled ?? !HTTPError.isError(error);
const { status = 500, statusText = "" } = unhandled ? {} : error;

if (status === 404) {
const url = (event as H3Event).url || new URL(event.req.url);
const baseURL = import.meta.baseURL || "/";
if (/^\/[^/]/.test(baseURL) && !url.pathname.startsWith(baseURL)) {
const redirectTo = `${baseURL}${url.pathname.slice(1)}${url.search}`;
return {
status: 302,
statusText: "Found",
headers: { location: redirectTo },
body: `Redirecting...`,
headers: new Headers({ location: `${baseURL}${url.pathname.slice(1)}${url.search}` }),
};
}
}

// Console output
if (isSensitive && !opts?.silent) {
// prettier-ignore
const tags = [error.unhandled && "[unhandled]"].filter(Boolean).join(" ")
console.error(`[request error] ${tags} [${event.req.method}] ${url}\n`, error);
}

// Send response
const headers: HeadersInit = {
"content-type": "application/json",
"x-content-type-options": "nosniff",
"x-frame-options": "DENY",
"referrer-policy": "no-referrer",
"content-security-policy": "script-src 'none'; frame-ancestors 'none';",
};

if (status === 404 || !(event as H3Event).res.headers.has("cache-control")) {
headers["cache-control"] = "no-cache";
}

const body = {
error: true,
url: url.href,
status,
statusText: error.statusText,
message: isSensitive ? "Server Error" : error.message,
data: isSensitive ? undefined : error.data,
};
const headers = new Headers(unhandled ? {} : error.headers);
headers.set("content-type", "application/json; charset=utf-8");

return {
status,
statusText: error.statusText,
statusText,
headers,
body,
body: {
error: true,
...(unhandled ? { status, unhandled: true } : error.toJSON?.()),
},
};
}
8 changes: 4 additions & 4 deletions src/runtime/internal/error/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export function defineNitroErrorHandler(handler: NitroErrorHandler): NitroErrorH
}

export type InternalHandlerResponse = {
status: number;
statusText: string | undefined;
headers: Record<string, string>;
body: string | Record<string, any>;
status?: number;
statusText?: string | undefined;
headers?: Headers;
body?: string | Record<string, any>;
};
8 changes: 0 additions & 8 deletions test/fixture/server/routes/api/error.ts

This file was deleted.

22 changes: 22 additions & 0 deletions test/fixture/server/routes/errors/throw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { H3Event, HTTPError } from "nitro/h3";

export default ({ url }: H3Event) => {
const unhandled = url.searchParams.has("unhandled");
const shouldThrow = url.searchParams.get("action") === "throw";

const error = unhandled
? new Error("Unhandled error")
: new HTTPError({
status: 503,
statusText: "Custom Status Text",
message: "Handled error",
data: { custom: "data" },
body: { custom: "body" },
});

if (shouldThrow) {
throw error;
}

return error;
};
26 changes: 13 additions & 13 deletions test/presets/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,16 @@ describe("nitro:preset:vercel:web", async () => {
"src": "/fetch",
},
{
"dest": "/error-stack",
"src": "/error-stack",
"dest": "/errors/throw",
"src": "/errors/throw",
},
{
"dest": "/errors/stack",
"src": "/errors/stack",
},
{
"dest": "/errors/captured",
"src": "/errors/captured",
},
{
"dest": "/env",
Expand Down Expand Up @@ -303,14 +311,6 @@ describe("nitro:preset:vercel:web", async () => {
"dest": "/api/headers",
"src": "/api/headers",
},
{
"dest": "/api/errors",
"src": "/api/errors",
},
{
"dest": "/api/error",
"src": "/api/error",
},
{
"dest": "/api/echo",
"src": "/api/echo",
Expand Down Expand Up @@ -421,8 +421,6 @@ describe("nitro:preset:vercel:web", async () => {
"functions/api/cached.func (symlink)",
"functions/api/db.func (symlink)",
"functions/api/echo.func (symlink)",
"functions/api/error.func (symlink)",
"functions/api/errors.func (symlink)",
"functions/api/headers.func (symlink)",
"functions/api/hello.func (symlink)",
"functions/api/hey.func (symlink)",
Expand All @@ -441,7 +439,9 @@ describe("nitro:preset:vercel:web", async () => {
"functions/config.func (symlink)",
"functions/context.func (symlink)",
"functions/env.func (symlink)",
"functions/error-stack.func (symlink)",
"functions/errors/captured.func (symlink)",
"functions/errors/stack.func (symlink)",
"functions/errors/throw.func (symlink)",
"functions/fetch.func (symlink)",
"functions/file.func (symlink)",
"functions/icon.png.func (symlink)",
Expand Down
Loading
Loading