fix: surface app errors in gateway logs

This commit is contained in:
2026-07-20 14:26:27 +05:30
parent aa2595ec01
commit 2b4083c6db
56 changed files with 147 additions and 86 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.2.68",
"version": "0.2.69",
"type": "module",
"main": "src/index.ts",
"exports": {
+30 -1
View File
@@ -128,6 +128,27 @@ function timingSafeEqual(a: string, b: string): boolean {
return diff === 0;
}
export function internalError(res: Response): string | null {
const encoded = res.headers.get("x-wrnexus-internal-error");
if (!encoded) return null;
try {
return decodeURIComponent(encoded);
} catch {
return "Malformed internal error diagnostic";
}
}
export function stripInternalError(res: Response): Response {
if (!res.headers.has("x-wrnexus-internal-error")) return res;
const headers = new Headers(res.headers);
headers.delete("x-wrnexus-internal-error");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers,
});
}
/** Preserve an intentional verifier redirect while keeping other failures opaque. */
export function forwardAuthFailure(res: Response, verifierUrl: string): Response {
const location = res.headers.get("location");
@@ -210,8 +231,9 @@ async function checkAuth(
if (!res.ok) {
if (res.status >= 500) {
const requestUrl = new URL(req.url);
const diagnostic = internalError(res);
console.error(
`[wrnexus] forward-auth verifier error: ${req.method} ${requestUrl.pathname} via ${verifyUrl} returned ${res.status}`,
`[wrnexus] forward-auth verifier error: ${req.method} ${requestUrl.pathname} via ${verifyUrl} returned ${res.status}${diagnostic ? `${diagnostic}` : ""}`,
);
}
return forwardAuthFailure(res, verifyUrl);
@@ -506,6 +528,13 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
);
res = new Response(`Gateway: app '${target.name}' is unavailable.`, { status: 502 });
}
const diagnostic = internalError(res);
if (res.status >= 500 && diagnostic) {
console.error(
`[wrnexus] app response error: ${target.name} ${req.method} ${url.pathname}${res.status}${diagnostic}`,
);
}
res = stripInternalError(res);
if (sec.headers) res = applyEdgeHeaders(res);
if (sec.accessLog)
console.log(
+7 -1
View File
@@ -791,10 +791,16 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
const app = process.env.WRNEXUS_APP_NAME ?? "app";
const detail =
err instanceof Error ? (err.stack ?? `${err.name}: ${err.message}`) : String(err);
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
console.error(
`[wrnexus] unhandled request error (${app}) ${req.method} ${url.pathname}\n${detail}`,
);
return compressResponse(req, secure(renderError(err, mode)));
const response = secure(renderError(err, mode));
// Gateway-managed production apps bind to loopback. Carry a bounded,
// encoded diagnostic to the parent gateway so centralized log collectors
// can explain child failures; the gateway always strips this header.
response.headers.set("x-wrnexus-internal-error", encodeURIComponent(message.slice(0, 500)));
return compressResponse(req, response);
}
}
+17
View File
@@ -5,6 +5,8 @@ import {
forwardAuthHeaders,
gatewayProxyHeaders,
gatewayRestartDelay,
internalError,
stripInternalError,
} from "../src/gateway.ts";
import { resolveProductionHostname } from "../src/prod.ts";
@@ -47,6 +49,21 @@ test("forward auth preserves intentional verifier redirects", () => {
expect(denied.headers.has("location")).toBe(false);
});
test("gateway reads and strips internal app diagnostics", async () => {
const response = new Response("safe public error", {
status: 500,
headers: {
"x-wrnexus-internal-error": encodeURIComponent('Error: Unknown workspace app "admin"'),
},
});
expect(internalError(response)).toBe('Error: Unknown workspace app "admin"');
const stripped = stripInternalError(response);
expect(stripped.status).toBe(500);
expect(stripped.headers.has("x-wrnexus-internal-error")).toBe(false);
expect(await stripped.text()).toBe("safe public error");
});
test("forward auth describes the original gateway request", () => {
const headers = forwardAuthHeaders(
new Request("https://admin.example.test/settings?tab=security", {