Skip to content

flux-network: return bound endpoints and remove a port-selection race - #145

Closed
Bronek wants to merge 5 commits into
bronek/api_server-bfrom
bronek/api_server-c
Closed

flux-network: return bound endpoints and remove a port-selection race#145
Bronek wants to merge 5 commits into
bronek/api_server-bfrom
bronek/api_server-c

Conversation

@Bronek

@Bronek Bronek commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

This PR builds on #144. listen now reports the endpoint it bound, tests bind ephemeral ports without a probe-then-bind race, and slice response bodies go directly to the framing path without first passing through the service's composition scratch.

Bound endpoints

StreamNetwork::listen(group, endpoint) and HttpService::listen(&mut net, endpoint) return io::Result<Endpoint> instead of io::Result<()>. The result matches the requested endpoint except when TCP port 0 is used, in which case it contains the port selected by the kernel; a Unix listener returns its path. A caller binding a fixed endpoint may ignore the value, while one requesting port 0 can publish an address that the listener already holds.

Response framing

respond(status, headers, body) passes the caller's slice directly to the framing path, while respond_with renders into the reusable service-owned buffer. Both use one framing implementation and copy the body into the send queue once. Admission requires a pending request, a status in 100..=599, and headers the service does not reserve; it is checked before composition, so a refused answer does not run the body closure. A HEAD request still uses the composed body length for Content-Length while sending none of the body.

Tests bind ephemeral ports

The integration suites and in-module harness now bind TCP port 0 and dial the endpoint returned by listen, so an address is not published until its listener holds it. The old helper released a probe before the real bind and, while avoiding reuse, could temporarily occupy a port already handed to another test thread; the victim then failed with AddrInUse.

Verification

Tests cover a composed response followed by a slice response, preventing stale scratch contents from leaking, and pin refusal before body composition on both response paths. Workspace tests, Clippy with warnings denied, the nightly formatting check, and cargo doc pass without new warnings.

Scope

The only API change is the return type of the two listen methods. The version bump that lists this and the stack's other breaking changes follows as the last PR.

Base: #144. Next: the 0.2.0 bump.

Refer: #143

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5

@Bronek
Bronek marked this pull request as draft August 28, 2026 14:28
A caller who already holds its body has nowhere to render it, so the
buffer the service keeps for composed bodies costs it a copy it never
needed: once into the scratch, once out of it. A HEAD request paid for
both and then discarded the result.

Framing now takes the bytes wherever the caller holds them. respond
hands write_framed the caller's slice, respond_with renders into the
scratch and hands it that, and the admission rule both share sits in
answerable, which respond_with also asks before composing anything. On
the service, one path builds the responder and reclaims what its answer
consumed, and the statuses the service raises itself carry an empty body
rather than an empty closure.

Assisted-by: Claude Code:claude-opus-5
A caller that asks for TCP port 0 lets the kernel choose the port and
then has no way to learn it, so anything that must dial the listener has
to pick a port up front. Picking one means binding a probe, reading its
address and releasing it, and between the release and the real bind the
port belongs to whoever takes it first.

listen now returns the endpoint it bound. It is the endpoint asked for
in every case but a TCP port of 0, where it carries the port the kernel
chose, so a caller binds first and hands out an address that is already
its own.

Assisted-by: Claude Code:claude-opus-5
Every test binary picked its loopback ports by binding a probe, reading
its address and releasing it, and one of them held colliding probes
bound while it looked for a free port -- squatting on an address it had
already handed to another thread. The victim's own bind then failed with
AddrInUse, once in roughly thirty-five runs of the HTTP suite.

Listeners now bind port 0 and the tests use the endpoint that comes
back, so an address exists only once something holds it. The transport
macros hand a body a request rather than a reservation, and the helpers
that build a server -- Http, Server, Peer -- report where they landed.

Two places still choose an address before it is bound, both because
TcpConnector::listen_at takes one and reports none back: the wire
compatibility test in stream_network.rs, which says so where it probes,
and the tcp_* connector tests, which never shared the squatting helper.

Assisted-by: Claude Code:claude-opus-5
The in-module harness listens on port 0 and dials the address the
listener reports, rather than probing for a free port and hoping it
stays free until the service binds it. The unit tests no longer race
anything else on the machine for a port.

Assisted-by: Claude Code:claude-opus-5
Both public answer paths ask answerable before anything is composed --
a refused status or header runs no body closure -- and write_framed,
which they share, frames what they hand it as admitted. The check moves
out of write_framed into respond, beside the one respond_with already
made, so a composed answer scans its headers once rather than twice; a
debug assertion keeps the invariant visible where the framing happens.

One admission rule, one framing implementation, one body copy for a
slice and no closure for a refused answer, as before.

Assisted-by: Claude Code:claude-fable-5
@Bronek
Bronek force-pushed the bronek/api_server-c branch from 97e76ad to f93d2ee Compare September 3, 2026 13:54
@Bronek Bronek closed this Sep 4, 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