Skip to content

h3 v1.15.7+ overwrites req.url with decoded path, breaking proxied requests with percent-encoded UTF-8 #1354

@sergioazoc

Description

@sergioazoc

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:

  1. Browser sends /_og/d/c_Blog,title_caf%C3%A9.png
  2. h3 decodes it to /_og/d/c_Blog,title_café.png and writes to req.url
  3. Nitro's httpxy proxy forwards req.url (now decoded) to the Nitro worker
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions