Skip to content

fix(routes): canonicalize path parameters so client calls match handlers (#505)#506

Merged
DeusData merged 1 commit into
DeusData:mainfrom
imateusdev:fix/route-param-canonicalization
Jun 23, 2026
Merged

fix(routes): canonicalize path parameters so client calls match handlers (#505)#506
DeusData merged 1 commit into
DeusData:mainfrom
imateusdev:fix/route-param-canonicalization

Conversation

@imateusdev

Copy link
Copy Markdown
Contributor

What does this PR do?

Canonicalizes route-path parameter placeholders when building Route QNs so that
a client HTTP_CALLS and the server handler that serves it rendezvous on the same
Route node even when each side uses a different framework's placeholder syntax
(or a different parameter name).

Fixes #505.

Problem

Route QNs (__route__METHOD__/path) are the rendezvous key, but they were built
from the raw path. A Rust/Axum handler GET /players/{id} and a TypeScript client
call /players/:id therefore produced different QNs and never joined. Static
routes joined fine; parameterized routes (the bulk of a REST surface) did not:

Route<-[:HTTP_CALLS]- ∩ Route<-[:HANDLES]-  ==  ∅   (for parameterized paths)

Fix

New shared helper cbm_route_canon_path() (declared in pipeline_internal.h,
defined in pass_route_nodes.c) collapses each parameter token to a single {}:

Input Canonical
/players/:id /players/{}
/players/{id} /players/{}
/users/<int:id> /users/{}
/players/${id} /players/{}
/orders/{id}/items/{itemIndex} /orders/{}/items/{}

Applied to the path at every HTTP-route QN construction site
(pass_route_nodes.c, pass_parallel.c, pass_calls.c, pass_cross_repo.c);
broker/async/infra QNs are left untouched. The Route node's display name keeps
the original path — only the QN (identity/dedup key) is canonicalized — so the
existing ANY-vs-method reconciliation does the rest, exactly as it already does
for static routes.

Design note / tradeoff

Parameter names are intentionally discarded: /users/{id} and /users/{slug}
canonicalize to the same node. For REST surfaces these are virtually always the
same logical endpoint; flagging it here for reviewer visibility.

Tests

tests/test_route_canon.c — 11 unit cases covering each placeholder syntax, the
:id{id} convergence invariant, parameter-name agnosticism, literal : mid
segment, multiple params, NULL/empty, and tight-buffer truncation safety.

Verification

  • make -f Makefile.cbm cbm — clean build (-Wall -Wextra -Werror).
  • make -f Makefile.cbm test — full ASan/UBSan suite; the new route_canon
    suite passes 11/11 and no previously-passing test regresses.
  • clang-format-18 --dry-run --Werror clean on all changed files.

Checklist

  • Every commit is signed off (git commit -s)
  • Tests pass locally
  • Lint passes (clang-format) on changed files
  • New behavior is covered by a test (reproduce-first)

@DeusData

Copy link
Copy Markdown
Owner

Thanks @imateusdev — the route-param canonicalization is correct and well-tested. The CI red here is a cppcheck false-positive: at src/pipeline/pass_route_nodes.c:59 it flags return out (in the out == NULL early-return) as an "uninitialized variable", which left the lint job cancelled and the test job skipped — it's not a real test failure.

Please add a targeted // cppcheck-suppress with a one-line WHY (returning the NULL pointer is intentional), or restructure the early-return so cppcheck is satisfied. Once lint runs to completion green, the test job unblocks and this is good to merge. 🙏

Route QNs (__route__METHOD__/path) are the rendezvous key for joining a client
HTTP_CALLS to the server handler that serves it, but they were built from the raw
path. Frameworks write path parameters with different placeholder syntaxes
(":id" Express/clients, "{id}" Axum/Spring/OpenAPI, "<id>" Flask/Rocket), so a
client call and its handler produced different QNs and never joined. Static
routes joined; parameterized routes (most of a REST surface) did not.

Add cbm_route_canon_path() which collapses each parameter token (":name",
"{name}", "<name>", "${...}") to a single "{}" token, and apply it to the path
at every HTTP-route QN construction site (pass_route_nodes, pass_parallel,
pass_calls, pass_cross_repo). Broker/async/infra QNs are left untouched. Only the
QN (identity/dedup key) is canonicalized; the Route node display name keeps the
original path. Parameter names are intentionally discarded so the same logical
endpoint matches across services that name the variable differently.

Adds tests/test_route_canon.c (11 cases). Fixes DeusData#505.

Signed-off-by: imateusdev <imateusdev@gmail.com>
@imateusdev imateusdev force-pushed the fix/route-param-canonicalization branch from d472dbc to 8b81367 Compare June 23, 2026 16:52
@imateusdev

Copy link
Copy Markdown
Contributor Author

Thanks @DeusData! Done — and a heads-up on a second issue I hit along the way.

1. cppcheck false-positive (pass_route_nodes.c) — fixed with a targeted inline suppression and a WHY note, since out is just the pointer being returned in the early-out (the buffer is never read here):

if (out == NULL || out_sz == 0) {
    /* WHY: cppcheck value-flow follows a caller that passes an as-yet
     * uninitialized output buffer (e.g. `char cpath[256];`) and flags this
     * `return out` as uninitvar. False positive: we only return the pointer
     * here and never read the buffer; returning it (NULL, or a buffer too
     * small to write into) is the intended early-out contract. */
    // cppcheck-suppress uninitvar
    return out;
}

2. Merge conflict with #485 — while pushing I noticed the branch had gone CONFLICTING: #485 (f5ea36c) landed on main and removed cbm_route_canon_path from pass_route_nodes.c in favor of the filesystem-path rejection (cbm_service_pattern_is_http_route_literal). I rebased onto current main and reconciled the two — they compose cleanly: #485's filter decides which URLs become routes, then this PR canonicalizes the path params on the survivors:

if (strcmp(edge->type, "HTTP_CALLS") == 0 &&
    !cbm_service_pattern_is_http_route_literal(url, callee)) {
    return;  // #485: reject filesystem paths
}
...
cbm_route_canon_path(url, cpath, sizeof(cpath))  // this PR: canonicalize survivors

Force-pushed the rebased commit (8b81367). CI is now fully green (lint + the full test matrix across all 5 platforms), and the PR is MERGEABLE / CLEAN. Should be good to merge 🙏

@DeusData DeusData merged commit 008f6e3 into DeusData:main Jun 23, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP route matching never joins client calls to handlers for parameterized paths (:id vs {id} placeholder mismatch)

2 participants