Skip to content

fix(frontend): collapse double slash when joining trailing-slash upstream base path - #308

Open
detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-frontend-collapse-double-slash-when-joining-tr-349b49
Open

detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-frontend-collapse-double-slash-when-joining-tr-349b49

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 16, 2026

Copy link
Copy Markdown

Detail bug report: View on Detail

Bug

UpstreamManager.buildUpstreamUri in src/frontend/upstream.zig joins the configured upstream URL's base path with the inbound request target to form the URI the proxy dials on every forwarded request. The join logic could only insert a missing / between the two halves; it had no branch to collapse a redundant / at the boundary. So when an operator configured an upstream URL whose path ends in / (e.g. https://internal-gateway.corp/datadog/) and a forwarded request arrived with the usual origin-form target starting with / (e.g. /api/v2/logs), the join wrote base_path verbatim followed by request_path verbatim, emitting …/datadog//api/v2/logs (two adjacent slashes) instead of …/datadog/api/v2/logs.

The malformed // survives std.Uri.parse (the next step at src/frontend/exec.zig:167) and reaches std.http.Client.request as the request target, so the proxy silently forwards to a different resource path than the operator intended. The shipped defaults all use path-less upstream URLs, so out-of-the-box deployments don't hit this; it only surfaces for the base-path configuration the join code exists to support.

Fix

Reworked the join in buildUpstreamUri so that when the written base path ends with / and the request path begins with /, the request's leading / is dropped (single / at the boundary). Otherwise a / is inserted only when neither side provides one. Storage in createUpstream is left verbatim, so getUpstreamConfig().base_path behavior is unchanged and the previously-correct join combinations are unaffected.

The fix is localized to the single copy of the join logic (verified via repo-wide search; there are no duplicates).

Testing

  • Unit tests inline in src/frontend/upstream.zig (registered via src/root.zig), added two regression guards:
    • buildUpstreamUri collapse boundary slashes — trailing-slash base × leading/no-leading request, with query, with empty path, and asserts the produced URI round-trips through std.Uri.parse to a path with no //.
    • buildUpstreamUri join boundary combinations — matrix over base_path ∈ {"", "/", "/v2", "/v2/"} × request_path shapes in one manager, covering the previously-correct branches as regression guards.
  • zig build test --summary all (Debug) and zig build test -Doptimize=ReleaseSafe --summary all both pass (0 failures). zig fmt --check, ziglint, task do, and task build:safe are clean; zig build -Dfrontend=httpz and zig build -Dfrontend=stdio both build.
  • End-to-end: ran the proxy against a mock upstream that captures the raw HTTP request line.
    • Trailing-slash upstream http://127.0.0.1:18080/datadog/ + inbound POST /api/v2/logs → mock received POST /datadog/api/v2/logs HTTP/1.1 (single /), client got 200 OK with the relayed body. With the bug this was /datadog//api/v2/logs.
    • Path-less upstream http://127.0.0.1:18080 → mock received POST /api/v2/logs HTTP/1.1 (default-config shape unaffected).
    • Non-trailing-slash base http://127.0.0.1:18080/datadog → mock received POST /datadog/api/v2/logs HTTP/1.1 (previously-correct path unaffected).
  • Could not verify the end-to-end stdio-frontend parity run: the stdio frontend aborts in the Zig 0.16.0 std.Io.Threaded backend during the upstream socket write (programmer bug caused syscall error: INVAL in std/Io/Threaded.zig), and single_threaded can't start its per-connection spawn (ConcurrencyUnavailable); evented/uring are rejected as having no networking in 0.16.0. This abort is in the upstream write phase (after buildUpstreamUri ran) and is a toolchain issue, not the fix. Both frontends share the identical join + upstream-open code at src/frontend/exec.zig:166-167, so the httpz wire results above exercise the fixed join path; a standalone zig test also confirms std.Uri.parse preserves // in the buggy string and produces none in the fixed string.

Fixes ENG-709


Automatic Fixes PRs can be configured here.

@macroscopeapp

macroscopeapp Bot commented Sep 16, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved at bfcd46d

Macroscope's review found this PR approvable — This is a small, self-contained URI-joining bug fix that removes redundant boundary slashes while preserving existing path and query behavior. Regression tests cover the relevant combinations and no broader runtime or API changes are introduced.

You can add or adjust custom eligibility rules. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant