Skip to content

WIP: fix(http): trailing slash vs missing path param - #232

Draft
HarshMN2345 wants to merge 1 commit into
mainfrom
fix/router-trailing-slash-param
Draft

WIP: fix(http): trailing slash vs missing path param#232
HarshMN2345 wants to merge 1 commit into
mainfrom
fix/router-trailing-slash-param

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Sep 8, 2026

Copy link
Copy Markdown
Member

Draft — the approach in this branch is wrong and is being reworked. Do not review the diff as it stands. The problems found are recorded below so the next attempt does not repeat them.

The bug

Router::match() strips every empty path segment before matching:

$parts = array_values(array_filter(explode('/', $path), fn($segment) => $segment !== ''));

So a trailing slash collapses onto the shorter route, and on an API that pairs a list route with a get route a missing ID becomes a full listing. Appwrite has carried this as appwrite/appwrite#6225 since 2023: GET /v1/functions/:functionId/deployments/ with an empty deploymentId returns 200 and every deployment of the function.

Why this branch does not fix it

The commit makes the router try the un-collapsed path first, so an empty trailing segment can bind to a :param. Probing it against a real route table shows three problems.

1. It mis-dispatches requests that have nothing to do with a missing ID. Trying the raw candidate first lets a longer param route take a request away from a static or wildcard route:

routes: /blog/authors   /blog/:post/:comment
  /blog/authors/   before: /blog/authors   after: /blog/:post/:comment  {"post":"authors","comment":""}

routes: /docs/*   /docs/:section/:page
  /docs/tutorial/  before: /docs/*         after: /docs/:section/:page  {"section":"tutorial","page":""}

The bound value is not an obviously-invalid empty ID — post=authors looks valid and reaches the wrong handler. In Appwrite, DELETE /v1/databases/transactions/ moves from delete-database (scope databases.write) to delete-transaction (scope documents.write) with an empty transaction ID. An earlier version of this description claimed wildcard routes were unchanged; that was wrong.

2. It is incomplete. Only a single trailing slash is handled. Two or more still collapse:

routes: /blog   /blog/:post
  /blog/    -> /blog/:post {"post":""}
  /blog//   -> /blog
  /blog///  -> /blog

3. It reverses a deliberate contract. HttpTest::testNoMismatchRoute was not incidental. It came from utopia-php/http#96, "Fix route mismatch against shorter paths when params are missing", whose commit e8e12a8 asserts that /d/ must not match /d/:id. Trailing-slash tolerance arrived separately in 4114cf8, "add support for trailing slashes in routes and urls". Both are intentional, and #6225 sits in the gap between them. Rewriting that assertion silently picks a winner.

Where this leaves it

The router cannot tell a malformed get (getDeployment with an empty ID) from a caller requesting a slash-terminated list URL — both arrive as /v1/functions/x/deployments/. Only the SDK knows which operation was called, so that is where #6225 should be fixed: reject empty required path arguments in the generator templates, before the request is built. Required empty body and query values can be legitimate, and a falsy check would wrongly reject "0".

If stricter server-side routing is wanted on top of that, it needs to be a stated policy with a migration story — plausibly an opt-in flag, the way Express exposes strict routing — not a silent default change in a shared library.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Benchmark results

http — Swoole modes (4 cores, 200 VUs, 20s/run)

workload mode req/s p95
ok a 14022.831173/s 30.76ms
ok b 14920.659159/s 29.52ms
io a 460.36768/s 903.99ms
io b 3342.861076/s 51.66ms
cpu a 3281.027501/s 166.73ms
cpu b 3098.530581/s 85.74ms

a = HYPERLOOP_A (process), b = HYPERLOOP_B (coroutine)

Shared CI runners — treat absolute numbers as rough, compare modes within a run. Commit ba8d451.

`Router::match()` dropped every empty path segment before matching, so a
trailing slash silently collapsed onto the shorter route: `/blog/` matched
`/blog` rather than `/blog/:post`. On an API that pairs a list route with a
get route this turns a missing ID into a full listing.

Offer the empty trailing segment to a route that expects a param there
before dropping it. `/blog/` now reaches `/blog/:post` with an empty post,
which the param validator rejects, while `/about/` still falls back to
`/about` because no route expects a param at that position.
@HarshMN2345
HarshMN2345 force-pushed the fix/router-trailing-slash-param branch from 8b3d8ef to 124637b Compare September 8, 2026 10:56
@HarshMN2345
HarshMN2345 marked this pull request as draft September 8, 2026 11:33
@HarshMN2345 HarshMN2345 changed the title fix(http): bind a trailing slash to the param it leaves empty WIP: fix(http): trailing slash vs missing path param Sep 8, 2026
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.

1 participant