Description
Since v1.15.7, createAppEventHandler in h3 decodes percent-encoded URL paths and writes the decoded result back to event.node.req.url. This causes HTTP proxies (like Nitro's dev server using httpxy) to forward requests with raw UTF-8 characters instead of percent-encoded form, resulting in 400 Bad Request from downstream servers.
Affected versions
- h3 v1.15.7, v1.15.8, v1.15.9 (introduced by the security fixes for path traversal)
- Not affected: h3 v2.x RC (uses
event.url.pathname without mutating req.url)
Root cause
In createAppEventHandler (index.mjs, ~line 1991):
// Line 1995: _decodePath decodes %C3%A9 → é
const _reqPath = _decodePath(event._path || event.node.req.url || "/");
// Line 2014: decoded path written to req.url
event.node.req.url = _layerPath; // ← _layerPath is decoded
_decodePath (using decodePath from ufo) was added to prevent auth bypass via percent-encoded path segments. The decoded path is correct for internal routing (event._path), but should not overwrite event.node.req.url, which is consumed by HTTP proxies and Node.js middleware that expect the percent-encoded form.
Reproduction
Environment: Nuxt 4.4.2, Nitro 2.13.2, h3 1.15.9, Node.js v24
# Start a Nuxt dev server, then:
curl -s -w "%{http_code}" 'http://localhost:3000/_og/d/c_Blog,title_cafe.png'
# → 200 ✅ (ASCII only)
curl -s -w "%{http_code}" 'http://localhost:3000/_og/d/c_Blog,title_caf%C3%A9.png'
# → 400 ❌ (percent-encoded UTF-8)
The request flow:
- Browser sends
/_og/d/c_Blog,title_caf%C3%A9.png
- h3 decodes it to
/_og/d/c_Blog,title_café.png and writes to req.url
- Nitro's httpxy proxy forwards
req.url (now decoded) to the Nitro worker
- Worker's HTTP parser receives raw UTF-8 bytes in the request line → 400 Bad Request
Direct test on the Nitro worker (via Unix socket) returns 200 — confirming the worker handles the URL fine; the issue is the decoded req.url being proxied.
Real-world impact
nuxt-og-image v6 encodes component props (including page titles) in URL paths. For any non-ASCII language (Spanish, French, German, etc.), the titles contain accented characters that get percent-encoded. This makes OG image preview in Nuxt DevTools completely broken for all non-English content.
Suggested fix
Keep req.url in its original percent-encoded form. Use the decoded path only for internal routing (event._path):
function createAppEventHandler(stack, options) {
return eventHandler(async (event) => {
event.node.req.originalUrl = event.node.req.originalUrl || event.node.req.url || "/";
const _rawReqUrl = event.node.req.url || "/";
const _reqPath = _decodePath(event._path || _rawReqUrl);
event._path = _reqPath;
let _layerPath;
let _rawLayerPath;
// ...
for (const layer of stack) {
if (layer.route.length > 1) {
if (!_reqPath.startsWith(layer.route)) continue;
_layerPath = _reqPath.slice(layer.route.length) || "/";
_rawLayerPath = _rawReqUrl.slice(layer.route.length) || "/";
} else {
_layerPath = _reqPath;
_rawLayerPath = _rawReqUrl;
}
// ...
event._path = _layerPath; // decoded (for h3 internal use)
event.node.req.url = _rawLayerPath; // encoded (for proxies/middleware)
This preserves the security fix (decoded paths for route matching) while keeping req.url in the correct percent-encoded form for HTTP proxies.
Note
This is already fixed architecturally in h3 v2.x which does not mutate req.url at all. This issue is specifically for a v1.x patch release since Nuxt 4.x still depends on h3 v1.
Description
Since v1.15.7,
createAppEventHandlerin h3 decodes percent-encoded URL paths and writes the decoded result back toevent.node.req.url. This causes HTTP proxies (like Nitro's dev server usinghttpxy) to forward requests with raw UTF-8 characters instead of percent-encoded form, resulting in 400 Bad Request from downstream servers.Affected versions
event.url.pathnamewithout mutatingreq.url)Root cause
In
createAppEventHandler(index.mjs, ~line 1991):_decodePath(usingdecodePathfromufo) was added to prevent auth bypass via percent-encoded path segments. The decoded path is correct for internal routing (event._path), but should not overwriteevent.node.req.url, which is consumed by HTTP proxies and Node.js middleware that expect the percent-encoded form.Reproduction
Environment: Nuxt 4.4.2, Nitro 2.13.2, h3 1.15.9, Node.js v24
The request flow:
/_og/d/c_Blog,title_caf%C3%A9.png/_og/d/c_Blog,title_café.pngand writes toreq.urlreq.url(now decoded) to the Nitro workerDirect test on the Nitro worker (via Unix socket) returns 200 — confirming the worker handles the URL fine; the issue is the decoded
req.urlbeing proxied.Real-world impact
nuxt-og-imagev6 encodes component props (including page titles) in URL paths. For any non-ASCII language (Spanish, French, German, etc.), the titles contain accented characters that get percent-encoded. This makes OG image preview in Nuxt DevTools completely broken for all non-English content.Suggested fix
Keep
req.urlin its original percent-encoded form. Use the decoded path only for internal routing (event._path):This preserves the security fix (decoded paths for route matching) while keeping
req.urlin the correct percent-encoded form for HTTP proxies.Note
This is already fixed architecturally in h3 v2.x which does not mutate
req.urlat all. This issue is specifically for a v1.x patch release since Nuxt 4.x still depends on h3 v1.