π The bug
When the upstream fetch() in proxy-handler.ts rejects β timeout, ECONNRESET, DNS, TLS error, etc. β the catch block creates a 502 H3Error but doesn't attach the original error:
// dist/runtime/server/proxy-handler.js (v1.0.6, lines 252β258)
} catch (err) {
log("[proxy] Upstream error:", err);
throw createError({
statusCode: 502,
statusMessage: "Bad Gateway",
message: `Proxy upstream request failed: ${targetUrl}`
});
}
err goes through the debug-mode log() but isn't passed to createError. As far as I can tell, downstream observability (Sentry in our case) only sees the message string β there's no way to distinguish a timeout from a connection reset from a TLS handshake failure.
I might be wrong about this being unintentional β happy to be corrected if there's a reason err is stripped that I'm missing.
π οΈ To reproduce
I can put together a Stackblitz that points a proxied script at a deliberately slow upstream (e.g. httpbin.org/delay/30) to trip the 15s abort and inspect the resulting H3Error β let me know if that's needed before triage. The issue itself is visible in the source citation above.
π Expected behavior
The underlying error attached as cause (and optionally name/code surfaced in data) so error trackers can group and filter:
throw createError({
statusCode: 502,
statusMessage: "Bad Gateway",
message: `Proxy upstream request failed: ${targetUrl}`,
cause: err,
data: { errorName: err?.name, errorCode: err?.code },
});
H3's cause isn't serialized to the client response, so this should be safe β but you'd know better than me whether there's a reason to keep it stripped.
βΉοΈ Additional context
We're seeing a high volume of these in production (~800k/quarter, mostly from iOS in-app webviews proxying Meta Pixel) and have been stuck because the message alone doesn't tell us whether to look at the 15s timeout, network reset patterns, or something else.
Related context: I filed #639 about the v0 graceful-204 fallback being removed; that's resolved and 502s are the documented behavior now, which is fine β this issue is purely about making the 502 actionable.
Happy to send a PR if the change above looks right to you.
Environment: @nuxt/scripts@1.0.6, Nitro on Vercel (Node 22).
π The bug
When the upstream
fetch()inproxy-handler.tsrejects β timeout,ECONNRESET, DNS, TLS error, etc. β the catch block creates a 502 H3Error but doesn't attach the original error:errgoes through the debug-modelog()but isn't passed tocreateError. As far as I can tell, downstream observability (Sentry in our case) only sees the message string β there's no way to distinguish a timeout from a connection reset from a TLS handshake failure.I might be wrong about this being unintentional β happy to be corrected if there's a reason
erris stripped that I'm missing.π οΈ To reproduce
I can put together a Stackblitz that points a proxied script at a deliberately slow upstream (e.g.
httpbin.org/delay/30) to trip the 15s abort and inspect the resulting H3Error β let me know if that's needed before triage. The issue itself is visible in the source citation above.π Expected behavior
The underlying error attached as
cause(and optionallyname/codesurfaced indata) so error trackers can group and filter:H3's
causeisn't serialized to the client response, so this should be safe β but you'd know better than me whether there's a reason to keep it stripped.βΉοΈ Additional context
We're seeing a high volume of these in production (~800k/quarter, mostly from iOS in-app webviews proxying Meta Pixel) and have been stuck because the message alone doesn't tell us whether to look at the 15s timeout, network reset patterns, or something else.
Related context: I filed #639 about the v0 graceful-204 fallback being removed; that's resolved and 502s are the documented behavior now, which is fine β this issue is purely about making the 502 actionable.
Happy to send a PR if the change above looks right to you.
Environment:
@nuxt/scripts@1.0.6, Nitro on Vercel (Node 22).