unjs / unjs/httpxy

Empty request path is normalised to `/`, producing a spurious slash before the query when `prependPath` is on

Open Beginner friendly
#175 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
345
Forks
29
Avg merge
5d 10h
Merged PRs (30d)
2

Description

Environment
  • httpxy 0.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 && pathHasLeadingreturn 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.