feat: two HTTP servers behind one seam — the built-in backend - #8
Merged
Conversation
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
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.
Stacked on #7 (
feat/http-server-switch), which is where the switch itselflives. Base retargets to
mainautomatically when #7 merges.COR_HTTP_SERVER=builtinnow builds, links and serves. libmicrohttpd leaves theprocess entirely —
ldd corainenames it undermhdand does not underbuiltin, which is the check that the switch took.mhdbuiltin640 rather than 641 is the one test whose notification receiver has to serve
HTTPS; it carries
REQUIRE_HTTPSERVER: mhd. A valgrind pass over thenotification/subscription/distop cases under
builtinis 147/147,E:0 L:0 F:0.The split
corRestInit.cwas 1523 lines with ~68 of MHD scattered through it. It is nowthe shared half — service table, dispatch, worker pool, response policy — and
corRestBackend{Mhd,Builtin}.care the two halves that know a socket. Exactlyone is compiled; the makefile picks the source file as well as the
-Dpair, because compiling the unused one would need the library it exists to
avoid.
corRestBackend.his the contract, and it is internal.The response headers are built once, by
corRestResponseHeaderVBuild, inthe 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@contextcan 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 therequest 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 clientlibrary says otherwise, because a query string is what
application/x-www-form-urlencodedis for. Pythonrequests— and so theETSI suite — sends
q=name=="Eiffel Tower"asq=name%3D%3D%22Eiffel+Tower%22.Passing it through "correctly" costs six ETSI TPs and nothing else notices.
Also
corRestStateInittakes avoid*andCorRestState.hno longer includesmicrohttpd.h— ~2000 call sites included it transitively, and it was also whatsupplied
NULL. The archive is removed before it is rebuilt, or switchingflavours leaves both backends' objects in it.
The built-in server has no TLS, and
corRestInitrefuses to start ratherthan serving the port in the clear.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TGatXwrHx1CreL49sCuS37