Skip to content

Commit 51ca9b3

Browse files
authored
fix: preserve percent-encoded req.url in app event handler (#1355)
1 parent 4e8d43a commit 51ca9b3

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

src/app.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,18 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) {
140140
event.node.req.originalUrl =
141141
event.node.req.originalUrl || event.node.req.url || "/";
142142

143+
// Preserve the raw (percent-encoded) URL for proxies and Node.js middleware
144+
// that expect req.url in its original encoded form (RFC 3986).
145+
const _rawReqUrl = event.node.req.url || "/";
146+
143147
// Decode percent-encoded path segments to prevent auth bypass via encoding tricks.
144148
// Only decode the path portion, not the query string, to avoid double-decoding.
145-
const _reqPath = _decodePath(event._path || event.node.req.url || "/");
149+
const _reqPath = _decodePath(event._path || _rawReqUrl);
146150
event._path = _reqPath;
147151

152+
// Fast path: skip raw tracking when URL had nothing to decode
153+
const _needsRawUrl = _reqPath !== _rawReqUrl;
154+
148155
// Layer path is the path without the prefix
149156
let _layerPath: string;
150157

@@ -169,9 +176,14 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) {
169176
continue;
170177
}
171178

172-
// 3. Update event path with layer path
179+
// 3. Update event path (decoded for h3 internal routing)
180+
// and req.url (raw encoded for HTTP proxies and Node.js middleware)
173181
event._path = _layerPath;
174-
event.node.req.url = _layerPath;
182+
event.node.req.url = _needsRawUrl
183+
? layer.route.length > 1
184+
? _rawReqUrl.slice(layer.route.length) || "/"
185+
: _rawReqUrl
186+
: _layerPath;
175187

176188
// 4. Handle request
177189
const val = await layer.handler(event);

test/security.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,25 @@ describe("path decoding: no regressions", () => {
280280
await req2.get("/test/%61bc");
281281
expect(capturedOriginalUrl).toBe("/test/%61bc");
282282
});
283+
284+
it("req.url preserves percent-encoded UTF-8 characters", async () => {
285+
const app2 = createApp({ debug: false });
286+
let capturedReqUrl: string | undefined;
287+
app2.use(
288+
eventHandler((event) => {
289+
capturedReqUrl = event.node.req.url;
290+
return { path: event.path, reqUrl: capturedReqUrl };
291+
}),
292+
);
293+
const req2 = supertest(toNodeListener(app2)) as any;
294+
// %C3%A9 is the percent-encoded form of "é" (UTF-8)
295+
const res = await req2.get("/test/caf%C3%A9");
296+
expect(res.status).toBe(200);
297+
// event.path should be decoded (for h3 internal routing)
298+
expect(res.body.path).toBe("/test/café");
299+
// req.url must stay percent-encoded (for HTTP proxies and middleware)
300+
expect(res.body.reqUrl).toBe("/test/caf%C3%A9");
301+
});
283302
});
284303

285304
describe("path decoding with useBase", () => {

0 commit comments

Comments
 (0)