Skip to content

feat: two HTTP servers behind one seam — the built-in backend - #8

Merged
kzangeli merged 1 commit into
mainfrom
feat/corhttp-builtin-backend
Sep 5, 2026
Merged

feat: two HTTP servers behind one seam — the built-in backend#8
kzangeli merged 1 commit into
mainfrom
feat/corhttp-builtin-backend

Conversation

@kzangeli

@kzangeli kzangeli commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #7 (feat/http-server-switch), which is where the switch itself
lives. Base retargets to main automatically when #7 merges.

COR_HTTP_SERVER=builtin now builds, links and serves. libmicrohttpd leaves the
process entirely — ldd coraine names it under mhd and does not under
builtin, which is the check that the switch took.

functests ETSI
mhd 641/641
builtin 640/640 1046/1046

640 rather than 641 is the one test whose notification receiver has to serve
HTTPS; it carries REQUIRE_HTTPSERVER: mhd. A valgrind pass over the
notification/subscription/distop cases under builtin is 147/147, E:0 L:0 F:0.

The split

corRestInit.c was 1523 lines with ~68 of MHD scattered through it. It is now
the shared half — service table, dispatch, worker pool, response policy — and
corRestBackend{Mhd,Builtin}.c are the two halves that know a socket. Exactly
one is compiled; the makefile picks the source file as well as the -D
pair, because compiling the unused one would need the library it exists to
avoid. corRestBackend.h is the contract, and it is internal.

The response headers are built once, by corRestResponseHeaderVBuild, in
the order they go out — several hundred functests compare captured responses
line by line, and two copies of that policy would be two chances to drift. Same
for the § 6.3.4 / § 6.3.2 body checks and the query parser.

Two bugs this found, neither of them on the plan

The post-response phase cannot run on the I/O thread. Dispatching a
notification compacts it with the subscription's @context, and that @context
can be one the broker hosts itself. Fine on MHD's pool of I/O threads; a
deadlock on a single event loop. It did not hang, which is worse — the download
timed out, the notification went out 10 s late with an uncompacted body, and
nothing in the log said why. Now a second worker phase
(corRestAsyncFinish), which is in turn why the built-in backend copies the
request out of corHttp's read buffer: the phase runs after the connection has
moved on. MHD's path is unchanged.

+ in a query string is a space. RFC 3986 says literal plus; every client
library says otherwise, because a query string is what
application/x-www-form-urlencoded is for. Python requests — and so the
ETSI suite — sends q=name=="Eiffel Tower" as q=name%3D%3D%22Eiffel+Tower%22.
Passing it through "correctly" costs six ETSI TPs and nothing else notices.

Also

corRestStateInit takes a void* and CorRestState.h no longer includes
microhttpd.h — ~2000 call sites included it transitively, and it was also what
supplied NULL. The archive is removed before it is rebuilt, or switching
flavours leaves both backends' objects in it.

The built-in server has no TLS, and corRestInit refuses to start rather
than serving the port in the clear.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TGatXwrHx1CreL49sCuS37

COR_HTTP_SERVER=builtin now builds, links and serves. libmicrohttpd leaves the
process entirely: `ldd coraine` names it under `mhd` and does not under
`builtin`, which is the check that the switch took.

    full build (mhd)      641 tests, 641 passed
    built-in              640 tests, 640 passed     (- the one HTTPS receiver)
    ETSI, built-in       1046 TPs,  1046 passed

THE SPLIT. corRestInit.c was 1523 lines with ~68 of MHD scattered through it.
It is now the shared half - the service table, the dispatch, the worker pool,
the response policy - and corRestBackend{Mhd,Builtin}.c are the two halves that
know a socket. Exactly one is compiled; the makefile picks the SOURCE FILE as
well as the -D pair, because compiling the unused one would need the library it
exists to avoid. corRestBackend.h is the contract, and it is internal: nothing
above corRest includes it.

WHAT MOVED INTO SHARED CODE RATHER THAN BEING WRITTEN TWICE. The response
headers - content type, Preference-Applied, the service routine's own, CORS -
are built ONCE by corRestResponseHeaderVBuild, in the order they go out, because
several hundred functional tests compare captured responses line by line and two
copies of that policy would be two chances to drift. Same for the § 6.3.4 / §
6.3.2 body checks (corRestBodyPolicyCheck) and the query parser.

WHAT THE BUILT-IN BACKEND HAS TO DO THAT MHD DID FOR US:

  PERCENT-DECODING - MHD hands over a decoded path and decoded query values and
  corHttp decodes nothing, so the path is decoded here (after the query is split
  off, so a `%3F` in a path cannot masquerade as the delimiter) and the query
  goes through corRestUriParamsParse, the same split-and-decode the in-process
  self-forward already used. INCLUDING '+' MEANS SPACE, which RFC 3986 does not
  ask for and every HTTP client library produces anyway: Python requests, and so
  the ETSI suite, puts `q=name=="Eiffel Tower"` on the wire as
  `q=name%3D%3D%22Eiffel+Tower%22`. Read literally that matches nothing, with a
  200 and an empty array to show for it - six ETSI TPs, found exactly that way.

  THE BODY LIMIT - MHD streams and can be told to stop mid-body; corHttp
  delivers a whole request or none. Its cap is set from corRest's own threshold
  and over it corHttp hands the request up WITHOUT its body, which is precisely
  what corRestBodyPolicyCheck wants: it answers from the Content-Length header,
  so the ProblemDetails is identical either way.

  COPIES - corHttp is zero-copy into the connection's read buffer, and the
  post-response phase runs after the connection has moved on. Method, headers
  and body are copied into the request arena, which is what lets the request
  state outlive the connection it arrived on. MHD needs none of this; it owns
  copies of its own until NOTIFY_COMPLETED.

⭐ THE POST-RESPONSE PHASE IS NOW A WORKER PHASE (corRestAsyncFinish). It was
running on the I/O thread, which is fine on MHD's pool of them and is a DEADLOCK
on a single event loop: dispatching a notification compacts it with the
subscription's @context, that @context can be one THIS BROKER HOSTS, and the
loop thread then waits for an answer only the loop thread can produce. It does
not hang forever, which is worse - the download times out, the notification goes
out TEN SECONDS LATE with an uncompacted body, and nothing in the log says why.
Two functests caught it. The MHD backend keeps running it inline, unchanged.

Also: corRestStateInit takes a void* connection and CorRestState.h no longer
includes microhttpd.h - it is included by ~2000 call sites in the layers above,
and naming one server's type in it made every one of them depend on that server
being the one in the build. The archive is removed before it is rebuilt, or
switching flavours leaves both backends' objects in it and the link finds two
definitions of everything.

TLS: the built-in server has none, and corRestInit REFUSES to start rather than
serving the port in the clear. Only ftClient asks for it (the broker never
does), so one functional test carries REQUIRE_HTTPSERVER: mhd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGatXwrHx1CreL49sCuS37
Base automatically changed from feat/http-server-switch to main September 5, 2026 20:45
@kzangeli
kzangeli merged commit 7c8332a into main Sep 5, 2026
@kzangeli
kzangeli deleted the feat/corhttp-builtin-backend branch September 5, 2026 20:45
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