http: a client — response parser, url type, https routing point - #33
Merged
Conversation
alii
force-pushed
the
http-client
branch
2 times, most recently
from
August 13, 2026 04:47
e15a486 to
caad60f
Compare
T-118. scarlet/http was server-only: h1 had parse_request and no response parser, and no code anywhere turned an https:// string into a transport. h1 gains parse_response/response_framing, and a new module scarlet/http/url gives Scheme as a value so http-vs-https is decided once. scarlet/http/client is the request/response path and the routing point. TRANSPORT SHAPE. Socket and TlsSocket are different types on purpose, so the client takes an `Io` of three closures over one connection; plain(Socket) and secure(TlsSocket) build them. Rejected: duplicating send per transport (the response reader is where framing bugs live, two copies is two places to fix a smuggling reject in one of), and a Transport sum matched per read/write (keeps the types, three small matches, but closed — an Io backed by a Binary is how the whole path is tested with no network, and a Fake arm on a production sum type is a test hook on the wire). What shape 3 gives up is that all three closures address one connection, which needs an existential: T-217. ONE HEADER GRAMMAR. parse_response is a VM op, not Scarlet, so the field block below the status line is literally parse_header_block — the same code as a request head. Watched: deleting the whitespace-before-colon reject turns http_parse AND http_response red from one edit. parse_header_block's Bad now carries FieldReject (Malformed/TooLarge) instead of a request status code, so neither caller needs a `_` arm for a condition it cannot produce. RESPONSE FRAMING IS NOT REQUEST FRAMING. Absent framing fields mean no body on a request and until-close on a response, so ResponseFraming is its own type; reusing Framing truncates every close-framed reply to empty (watched). NOT DONE, deliberately: no connection pool (every request is Connection: close, so an https request is a handshake — the ticket's DONE-WHEN asked for pooling and this is scoped short of it), no redirects, cookies, retry, or streaming response bodies. No read deadline on either scheme: net/tls has no read_within (T-128), and a deadline only the cleartext side honours makes liveness depend on the URL scheme. Constant-pool ceiling in dis.rs raised 600 -> 1000: measured 520 without the two new modules, 604 with. Dedup was working; the ceiling was snug. T-219.
#36 and #33 each merge clean and the merged tree does not build: 14 calls expect a bare Binary. Fourteen unwraps would restore exactly the silence #36 exists to remove, with a Result in the signature to make it look handled, so every site was classified instead. FIVE were the tail, slice_bytes(b, at, byte_size(b) - at). drop_bytes already names that operation and is honestly total, so they carry no Err arm at all: url.split_bracketed's rest, url.split_at_colon's port_text, url.port_of's digits, client.read_head's rest, http_response's wire_loop. ONE takes master's own idiom, result.then(slice_bytes(..), to_string) or '?', byte-identical to what #36 did to http_parse.scrl's twin line. ONE is a genuine error and is now visible. http_response's "consumed threads to the body" asserts a 5-byte window against a Content-Length of 5, so a consumed that does not thread there has to print as an error rather than as an empty window. SEVEN carry an Err arm argued unreachable, each naming the bound it rests on: an index_of hit, first_of's clamp to size, the leading-[ guard, the byte_size >= n guard, int.min. That is an argument in a comment rather than a type, which is T-286. Six of the seven fall through to a loud error anyway (UnsupportedScheme, EmptyHost, MalformedResponse); target_of's does not, and says so. url.scrl's malformed-URL cases turn out to be rejected before any window is computed — scheme_sep, split_bracketed and port_of all fail first — so none of its 8 became a new UrlError variant. A variant nothing can construct is worse than the comment it replaces. Not done here: those 7 are all one operation, "cut b at a position something else already proved is inside b". drop_bytes names the tail half and the head half has no name. T-309 carries the site list and the argument that split_at_bytes is total for drop_bytes's own reason, and is not the take_bytes #36 refused — window_bytes(b, from, to) is, which is why it is not that. dis.rs's pool ceiling re-measured on the merged tree: 714 entries, not the 712 the comment claimed. The 1000 ceiling is unchanged and not close. Plants, all watched red and restored. #33's load-bearing one first: dropping the whitespace-before-colon disjunct in vm/http.rs reds http_parse AND http_response from that one edit, rc=101 — the response head parser is still literally the request head parser after a 14-site edit. Then two of mine. Truncating the framed fixture prints "OUT OF RANGE" in place of a short window, rc=101, so the genuine error reaches the caller. Weakening read_exactly's guard to >= n - 1 prints Err MalformedResponse(BadFraming) in place of a truncated body, rc=101, so the argued-unreachable arm is live and loud when its bound is wrong. fmt 0, clippy -D warnings 0, test --workspace 0, gen-editor-syntax --check 0, SCARLET_GC_STRESS=1 0, dylint --all 0 with the cache cleared and sources touched (0 findings), hawk check -D warnings 0 (0 findings). Filed alongside: T-309 (split_at_bytes), T-310 (h1.scrl's HeadFlags still carries two connection bools where the Rust side is one ConnTokens).
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.
scarlet/httpwas server-only. This adds the response parser, aUrltype, and the routing point that picks cleartext or TLS — unblocked by#26landing TLS.Transport shape: parameterise, not a sum type
SocketandTlsSocketare deliberately different types, so the three shapes were: duplicate per transport, aTransportsum the client matches on, or parameterise over the transport's operations.Duplication loses because the response reader is where framing bugs live, and two copies means two places to fix one smuggling reject.
The sum type is not absurd — it keeps the types across three small matches. It loses because it is closed, and what it forfeits is a transport backed by a
Binary, which is how the whole request/response path is tested with no network. Adding aFakearm to a production sum to recover that is a test hook on the wire.Iowas made opaque and then reverted: opaque leavesplain/secureas the only doors, which shuts out the in-memory transport that motivated the shape in the first place.What parameterising gives up, stated rather than hidden: nothing proves all three closures address one connection. That needs an existential —
Io(h)would carry it, but thenIo(Socket)andIo(TlsSocket)differ andconnecthas no return type. Filed T-217; the cost is one unenforced invariant on a public type.One grammar, not two
The parser is a VM op, so the header block below a status line is literally
parse_header_block— the same code as a request head. That is the load-bearing claim, and plant 2 proves it: dropping the whitespace-before-colon reject turns both goldens red from one edit,http_parseandhttp_responsetogether.Plants — all watched red, all restored
code=200 body="Content-Length: 0\r\n\r\n"Content-Lengthbody="he"instead ofErr Transport(UnexpectedEof)X-Injected: yeson the wire as its own headerclient.getOne plant was inert and was replaced — disabling the obs-fold reject left the golden green, because obs-fold is caught twice. That is the only reason the check that shipped is known to be real. Two further plants tripped checks the author did not write: a no-progress
read_moremaderead_headspin rather than fail, and adding 7 ABI slots without binding them trippedfixture_binds_every_slot.Gates
fmt0 ·clippy0 ·gen-editor-syntax --check0 ·test --workspace0 — 37 binaries, 1265 passed (baseline 1264; +1 is the new golden).core_irgoldens: pure +512 type-id shift, 28 line pairs, 2 ids. The shift checker was itself planted twice — a changed variant index givesNOT A SHIFT: - ctor 7936.0 / + ctor 8448.1, an inconsistent id givestype id 7936 maps to both 8448 and 9999.Deliberately not done
No connection pool, so T-118's own DONE-WHEN ("over a pooled connection") is not met — every request sends
Connection: closeand an https request is a handshake. Filed T-220, and T-165 should not be closed on this alone.No read deadline on either scheme.
net/tlshas noread_within(T-128), and giving only the cleartext adapter a deadline would make liveness depend on the URL scheme — invisible until the https request is the one that hangs.Also filed T-219:
dis.rs's constant-pool ceiling was< 600, measured at 520 without these files and 604 with. Dedup was working; the ceiling was snug against stdlib size with 15% headroom. Raised to 1000, but the assertion tests the wrong thing — the failure it guards is multiplicative.Goes stale on merge
The website's TLS page says "There is no HTTP client in the standard library. An HTTPS request is request bytes written by hand over a
TlsSocket." Both sentences are false as of this branch. Separate repo, needs its own ticket.