http: no route slot takes a callback (#877) - #890
Merged
Conversation
`http_route`'s third element is a response BODY, but the signature named it
`handler` — which means "callback" in every mainstream framework. Passing a
function stringified it into the route table, so a live endpoint answered
200 with the debug repr `<fn hello>`: silent, remote-visible, and logged
nowhere.
Wider than filed. The issue reports the 3-element body slot; testing every
slot found the 4-element code form has the same hole — a function as the
source stringifies to `<fn hello>`, gets compiled as EigenScript, and
serves `null` with a 200:
/fn -> <fn hello> [200] as filed
/builtin -> <builtin> [200] as filed
/code-fn -> null [200] NOT in the issue
So the guard covers every slot — method, path, kind, body/source. VAL_FN
and VAL_BUILTIN are the only value types with no sensible rendering; dicts,
lists, numbers, buffers and text-builders all stringify to something a
client can use, so that is the whole of the class, and each slot gets a
message naming its own next step.
The check runs BEFORE anything is allocated or stored. rt_error sets the
error flag and returns rather than unwinding, so raising after the existing
value_to_string calls would strand method/path in a route slot that
route_count never reaches — a leak on the error path. Registration time is
also the right moment: the error lands on the line the author wrote, before
the socket is listening, instead of on a client's request.
Naming, since that is what invites the mistake: `handler` -> `body` in the
BUILTINS signature, in the four lib/http.eigs wrappers (which propagated it
verbatim) and their STDLIB entries, and in http_route's arity error.
Suite HS35 in test_http_server.sh pins all six rejections, that all four
valid body forms still register, and that an uncaught callable aborts
before http_serve. Two notes on the test itself, both found by the planted
fault rather than by reading it:
- The startup case is bounded by `timeout` because a regression makes
http_serve reachable and the server runs forever — unbounded, the case
would HANG the suite instead of failing it. It now reports rc=124.
- A fourth assertion ("nothing listens on the port") was written and then
removed: it PASSED under the planted fault, because timeout had already
killed the server before the check ran. A test that cannot fail when it
matters is noise.
Validated with a planted fault: with route_slot_is_callable stubbed to 0
and the binary rebuilt, all three HS35 assertions fail in ~5s.
Gates: release 3786/3786, asan-http + UBSan detect_leaks=1 3916/3916 with
the LeakSanitizer tally at its 0 floor, HTTP integration suite 45/45.
Closes #877
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a class of silent, remote-visible HTTP routing bugs by rejecting callable values (functions/builtins) in any http_route slot at registration time, and aligns the public-facing naming/docs to clarify that the 3rd element is a literal response body (not a callback). It also adds an integration test to pin the new behavior and prevent regressions that could otherwise hang the test suite.
Changes:
- Add registration-time validation in
http_routeto reject callable values in method/path/kind/body/source slots. - Rename the documented/stdlib-facing parameter from
handler→bodyto reduce “callback” confusion. - Add HS35 integration coverage for all callable-slot rejections and for abort-before-
http_servebehavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/ext_http.c |
Adds callable-slot guard and updates http_route arity message. |
tests/test_http_server.sh |
Adds HS35 integration assertions for callable-slot rejection and startup abort behavior. |
lib/http.eigs |
Renames stdlib wrapper parameters to body and updates wrapper comments. |
docs/STDLIB.md |
Updates stdlib docs to reflect body parameter and “not a callback” guidance. |
docs/BUILTINS.md |
Updates builtin signatures/docs to use body and document the new rejection behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+570
to
+571
| | `http_route` | `http_route of [method, path, body]` or `[method, path, "code", source]` | Register a route. `body` is a literal response body, **not** a callback — passing a function raises (#877); use the `code` form for per-request logic | | ||
| | `http_route_authed` | `http_route_authed of [method, path, body]` or `[method, path, "code", source]` | Register authenticated route; auth source published via `shared_set of ["require_auth", "<source>"]` | |
Comment on lines
+822
to
+833
| # `timeout` is load-bearing, not belt-and-braces: if this guard ever regresses, | ||
| # http_serve IS reached and serves forever, so an unbounded run would HANG the | ||
| # suite instead of failing it. Verified against a planted fault — without the | ||
| # guard this block reports rc=124 and fails, in ~5s. | ||
| OUT5=$(timeout 5 "$EIGS" "$SRV5" 2>&1); RC5=$? | ||
| if [ "$RC5" = "124" ]; then | ||
| fail "HS35 callable body reached http_serve — server ran until killed" "output: $OUT5" | ||
| elif [ "$RC5" != "0" ] && ! printf '%s\n' "$OUT5" | grep -q 'UNREACHABLE-SERVE'; then | ||
| ok "HS35 an uncaught callable body aborts before http_serve (rc=$RC5)" | ||
| else | ||
| fail "HS35 callable body did not stop startup" "rc=$RC5 output: $OUT5" | ||
| fi |
Comment on lines
+250
to
+274
| for (int i = 0; i < 2; i++) { | ||
| if (route_slot_is_callable(arg->data.list.items[i])) { | ||
| rt_error(EK_TYPE, 0, "http_route: %s must be a string, not a function", | ||
| i == 0 ? "method" : "path"); | ||
| return make_null(); | ||
| } | ||
| } | ||
| if (arg->data.list.count >= 4) { | ||
| if (route_slot_is_callable(arg->data.list.items[2])) { | ||
| rt_error(EK_TYPE, 0, "http_route: kind must be a string, not a function " | ||
| "(expected \"code\" or \"static\")"); | ||
| return make_null(); | ||
| } | ||
| if (route_slot_is_callable(arg->data.list.items[3])) { | ||
| rt_error(EK_TYPE, 0, "http_route: the code form's source must be a string of " | ||
| "EigenScript source, not a function — " | ||
| "http_route of [method, path, \"code\", \"return 42\"]"); | ||
| return make_null(); | ||
| } | ||
| } else if (route_slot_is_callable(arg->data.list.items[2])) { | ||
| rt_error(EK_TYPE, 0, "http_route: body must be a value, not a function — pass a " | ||
| "literal body (\"pong\"), or use the code form: " | ||
| "http_route of [method, path, \"code\", \"<source>\"]"); | ||
| return make_null(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
http_route's third element is a response body, but the signature named ithandler— which means "callback" in every mainstream HTTP framework. Passing a function stringified it into the route table, so a live endpoint answered 200 with a debug repr, silently and remote-visibly:Wider than filed
The issue reports the 3-element body slot. Testing every slot found the 4-element
codeform has the same hole — a function as the source stringifies to<fn hello>, gets compiled as EigenScript, and servesnullwith a 200:So the guard covers every slot — method, path, kind, body/source — each with a message naming its own next step.
VAL_FN/VAL_BUILTINare the only value types with no sensible rendering; dicts, lists, numbers, buffers and text-builders all stringify to something a client can use, so that is the whole of the class.One constraint the issue didn't mention
rt_errorsets the error flag and returns rather than unwinding. Raising after the existingvalue_to_stringcalls would strand the strdup'd method/path in a route slot thatroute_countnever reaches — a leak on the error path. The guard therefore runs before any allocation.Registration time is also the right moment, per the issue: the error lands on the line the author wrote, before the socket is listening, instead of on a client's request.
Naming
Since the name is what invites the mistake:
handler→bodyin the BUILTINS signature, in the fourlib/http.eigswrappers (which propagated it verbatim into the stdlib) and their STDLIB entries, and inhttp_route's arity error.The test, and two things the planted fault caught in it
Suite HS35 pins all six rejections, that all four valid body forms still register, and that an uncaught callable aborts before
http_serve. Both of the following were found by running the fault, not by reading the test:timeout, and that is load-bearing. With the guard regressed,http_serveIS reached and the server runs forever — unbounded, the case would hang the suite rather than fail it. It now reports rc=124 as an explicit failure.timeouthad already killed the server before the check ran. A test that cannot fail when it matters is noise, so it is gone; the three remaining assertions carry the detection.Planted fault: with
route_slot_is_callablestubbed toreturn 0and the binary rebuilt, all three HS35 assertions fail in ~5s.Gates
asan-http+ UBSan,detect_leaks=1(
ext_http.cis sanitized viamake asan-http, notmake asan— the latter compiles withEIGENSCRIPT_EXT_HTTP=0.)Behavior change
A function in a route slot previously registered and served a 200; it now raises
type_mismatchat registration. That is the point of the fix, and there are no external consumers to migrate.Closes #877
🤖 Generated with Claude Code