Skip to content

fix(parameters): refuse @Connection on handlers that never receive a connection - #101

Merged
Upd4ting merged 3 commits into
mainfrom
fix/connection-handler-mode
Nov 9, 2026
Merged

Upd4ting merged 3 commits into
mainfrom
fix/connection-handler-mode

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Why

@Connection() is documented as WebSocket-only, but on an HTTP handler it passed undefined without any error. Route decorators now refuse it when the class is defined, in the modes that never receive a connection.

Fixes #76

What changed

  • ProcessCallback throws a TypeError and registers no route for the handler when a handler parameter receives @Connection in handler, prefix or postfix mode. websocket and monitor mode stay allowed.
  • The @Connection TSDoc, docs/4.parameters.md and the api-interface skill state the rule by mode.
  • The verify scenario for injected values now puts @Connection on a WebSocket handler and checks that a @Get handler is refused.

Scope

  • Class properties with @Connection stay allowed, because every handler of the controller shares them. They stay undefined outside a WebSocket request.
  • Handlers declared above a refused handler in the same class stay registered, as for every other TypeError from ProcessCallback.
  • A child method shares the parameter records of the parent method with the same name, so a @Get override of a parent @WebsocketHandler method that uses @Connection is refused too. The shared records also cause a separate bug, filed as A child controller's parameter decorator overwrites the parent handler's parameter of the same method name #100.

Tradeoffs

  • The issue also offered a docs-only fix. It leaves the silent undefined, so no test can fail on it.
  • Monitors stay allowed. @antelopejs/api runs them after the upgrade with the connection set, and a probe showed a @Monitor receives the socket on a WebSocket route.
  • Limiting the check to callback.length parameters was tried and rejected. Handlers with default, rest or wrapped parameters still received the connection.

Blast Radius

Code that used @Connection on an HTTP, prefix or postfix handler now fails when the class is defined, where it used to get undefined. WebSocket and monitor handlers are not affected.

Verification

  • src/tests/connection.test.ts: 4 of 5 tests fail on main and all 5 pass on this branch. The outputs are below.
  • antelopejs-verify: all 12 planned journeys pass on the packed build.
  • pnpm run checks: Checks: 9 passed, 0 failed.
flowchart TD
  A[Route decorator] --> B[ProcessCallback]
  B --> C{A handler parameter receives @Connection?}
  C -- no --> R[RegisterRoute]
  C -- yes --> D{mode is websocket or monitor?}
  D -- yes --> R
  D -- no --> E[TypeError when the class is defined, no route]
Loading
Test output on main (82bbb2a) with this branch's test file
  @Connection handler modes
    1) refuses a @Get handler and registers nothing
    2) refuses @Prefix and @Postfix handlers, which never see a connection
    ✔ accepts a @Monitor handler, which runs after a WebSocket upgrade
    3) refuses @Connection after a default-valued parameter of a @Get handler
    4) refuses a child @Get override that inherits the parent's @Connection
  1 passing (9ms)
  4 failing
  1) @Connection handler modes
       refuses a @Get handler and registers nothing:
     AssertionError [ERR_ASSERTION]: Missing expected exception (TypeError).
      at Context.<anonymous> (dist/tests/connection.test.js:26:31)
      at process.processImmediate (node:internal/timers:574:21)
  2) @Connection handler modes
       refuses @Prefix and @Postfix handlers, which never see a connection:
     AssertionError [ERR_ASSERTION]: Missing expected exception.
      at Context.<anonymous> (dist/tests/connection.test.js:47:31)
      at process.processImmediate (node:internal/timers:574:21)
  3) @Connection handler modes
       refuses @Connection after a default-valued parameter of a @Get handler:
     AssertionError [ERR_ASSERTION]: Missing expected exception.
      at Context.<anonymous> (dist/tests/connection.test.js:98:31)
      at process.processImmediate (node:internal/timers:574:21)
  4) @Connection handler modes
       refuses a child @Get override that inherits the parent's @Connection:
     AssertionError [ERR_ASSERTION]: Missing expected exception.
      at Context.<anonymous> (dist/tests/connection.test.js:128:31)
      at process.processImmediate (node:internal/timers:574:21)
Test output on this branch
  @Connection handler modes
    ✔ refuses a @Get handler and registers nothing
    ✔ refuses @Prefix and @Postfix handlers, which never see a connection
    ✔ accepts a @Monitor handler, which runs after a WebSocket upgrade
    ✔ refuses @Connection after a default-valued parameter of a @Get handler
    ✔ refuses a child @Get override that inherits the parent's @Connection
  5 passing (32ms)

@Upd4ting
Upd4ting force-pushed the fix/connection-handler-mode branch from d8da1a8 to 19dc6bd Compare November 9, 2026 14:58
@Upd4ting
Upd4ting merged commit 109b247 into main Nov 9, 2026
7 checks passed
@Upd4ting
Upd4ting deleted the fix/connection-handler-mode branch November 9, 2026 16:17
Upd4ting added a commit that referenced this pull request Nov 10, 2026
## Why

The verify-interface skill still listed #75 and
#76 as open known findings. Both are closed. #99 registers `@Get("")` at
the controller root, and #101 refuses `@Connection` on handlers that
never receive a connection. An agent reading the skill learned behaviour
that is no longer true. The `route-locations` journey also stripped one
trailing slash, so it pinned neither form of the root location.

## What changed

- `features/README.md`: the Known findings section is removed. Its table
held only #75 and #76, and its "No journey pins either behaviour" line
went with it.
- `features/routes.md`: the #75 gotcha is removed. The `route-locations`
sub-feature now names the root location it pins.
- `scenarios-second.ts`: `route-locations` compares `r.handler.location`
as registered and drops `.replace(/\/$/, "")`. The expected line already
held `/users` for `@Get("")`, so it now pins the root form exactly.
- `@Connection`: #101 already covers the refusal in the
`injected-values` journey (`connectionOnGet`, listed in
`features/parameter-sources.md`). This PR does not change it.

## Proof

Verify journeys the retired findings pointed at, run on this branch's
packed build:

```text
$ antelopejs-verify journey "$RUN" routes
PASS routes/route-locations (0.0 s)
PASS routes/route-errors (0.0 s)
$ antelopejs-verify journey "$RUN" parameter-sources injected-values
PASS parameter-sources/injected-values (0.0 s)
$ antelopejs-verify journey "$RUN" --plan
PASS smoke/core-calls (0.4 s)
PASS smoke/export-surface (0.0 s)
```

The same `route-locations` journey with `src/routes.ts` set back to the
pre-#99 join (`${controller.location}/${segment}`), a local change that
was not committed:

```text
FAIL routes/route-locations (0.0 s)
    02-run-the-route-locations-scenario: route-locations answers as docs/ states (expected "{\"routes\":[[\"handler\",\"get\",\"/users/listUsers\",\"listUsers\",null],[\"handler\",\"get\",\"/users\",\"root\",null],...
```

With the strip in place, that regression would pass.

`pnpm run checks`:

```text
Checks: 9 passed, 0 failed.
```

```mermaid
flowchart LR
  A["@get(&quot;&quot;) on Controller(&quot;/users&quot;)"] --> B[ProcessCallback]
  B --> C["location /users"]
  C --> D{"route-locations journey"}
  D -- "before: strip one trailing slash" --> E["/users and /users/ both pass"]
  D -- "after: compare as registered" --> F["only /users passes"]
```
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.

@Connection is documented as WebSocket-only but nothing enforces it

1 participant