Empty request path is normalised to `/`, producing a spurious slash before the query when `prependPath` is on
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 345
- Forks
- 29
- Avg merge
- 5d 10h
- Merged PRs (30d)
- 2
Description
Environment
httpxy0.5.5- Node 20 / 24 (reproduced on both)
Reproduction
Self-contained, no http-proxy-middleware involved — this uses httpxy directly:
import http from 'node:http';
import { createProxyServer } from 'httpxy';
const upstream = http.createServer((req, res) => res.end(`GOT:${req.url}`));
await new Promise((r) => upstream.listen(0, r));
const target = `http://127.0.0.1:${upstream.address().port}/addresses/summary`;
const proxy = createProxyServer({});
const front = http.createServer(async (req, res) => {
req.url = '?id=abc'; // empty path + query (e.g. after a full pathRewrite strip)
await proxy.web(req, res, { target, changeOrigin: true });
});
await new Promise((r) => front.listen(0, r));
const port = front.address().port;
http.get(`http://127.0.0.1:${port}/anything`, (r) => {
let b = '';
r.on('data', (c) => (b += c));
r.on('end', () => console.log('httpxy sent ->', b));
});
Describe the bug
When the incoming req.url has an empty path but a query string (e.g. ?id=abc) and the proxy target carries a path (so prependPath prepends it), httpxy inserts a spurious / between the target path and the query:
target: http://upstream/addresses/summary
req.url: ?id=abc
sent: /addresses/summary/?id=abc ← extra slash before "?"
expected: /addresses/summary?id=abc
Strict upstream routers (e.g. Ktor, some Go routers) return 404 on the extra slash. This is a behavioural regression from node-http-proxy, which dropped the empty path segment and sent /addresses/summary?id=abc.
An empty req.url path here means "no additional path — append the query to the target path", not "reset to root (/) then append". httpxy conflates the two.
Actual:
httpxy sent -> GOT:/addresses/summary/?id=abc
Expected:
httpxy sent -> GOT:/addresses/summary?id=abc
Additional context
Root cause
In the outgoing-path construction (dist/index.mjs, getOutgoingConfig):
const reqPath = qIdx === -1 ? reqUrl : reqUrl.slice(0, qIdx); // "" for "?id=abc"
const reqSearch = qIdx === -1 ? "" : reqUrl.slice(qIdx); // "?id=abc"
const normalizedPath = reqPath ? (reqPath[0] === "/" ? reqPath : "/" + reqPath) : "/"; // "" -> "/"
let outgoingPath = ... normalizedPath + reqSearch; // "/?id=abc"
let fullPath = joinURL(targetPath, outgoingPath); // joinURL("/addresses/summary", "/?id=abc")
joinURL then hits !baseHasTrailing && pathHasLeading → return base + path → /addresses/summary + /?id=abc.
The empty-path → "/" normalisation is the culprit: an empty rewritten path shouldn't introduce a path segment when a target path is being prepended.
For comparison, node-http-proxy's urlJoin split the query off first and ran args.filter(Boolean).join('/'), so an empty path segment was dropped and no extra slash was added.
Suggested fix
When reqPath is empty, don't force it to / before joining onto a prepended target path — treat the outgoing path as targetPath + reqSearch (i.e. append only the query). Roughly:
const outgoingPath =
reqPath === '' ? reqSearch // empty path: append query only
: normalizedPath + reqSearch;
so joinURL("/addresses/summary", "?id=abc") yields /addresses/summary?id=abc. A regression test for the empty path + query + path-bearing target case would guard it.
Impact
http-proxy-middleware v4 uses httpxy as its engine. Any consumer that rewrites a route's whole path away (pathRewrite) against a target that carries a path now emits the trailing slash and 404s on strict upstreams — see chimurai/http-proxy-middleware#1016.
Logs
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run the supplied Node.js reproduction first, then inspect the outgoing-path construction in dist/index.mjs, especially getOutgoingConfig and joinURL. Add coverage for an empty request path with a query and a path-bearing target, and confirm the upstream receives /addresses/summary?id=abc without the extra slash.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nodejs, typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 86/100