http: 101 Switching Protocols and a duplex connection (NETWORK slice 14) #74
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
The duplex connection of the live UI (WEB.md's fifth request).
Source
Acceptance criteria
Dependencies
None known.
Moved from the planning lists of the docs; the design record stays the specification.
[loop:torbscript] intake: in scope (author orion, label loop), source NETWORK.md slice 14, not a duplicate, no collision with a design record. TorbScript in
std/http; WebSocket framing is a second slice, refine splits it. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z[loop:torbscript] claim refine by session:loop-torbscript-20261024 until 2026-10-24T02:35:08Z
[loop:torbscript] decided by the loop: #74 is the first and only step of the WebSocket work that belongs to
std/http; the framing is not a second step of this issue. The issue body stays as written; this comment is the amendment and replaces its two acceptance criteria.Why the split lands here. NETWORK.md section 13, request 5, is explicit: the server answers
101 Switching Protocolswith an upgrade closure and hands it the connection'ssource()andsink(); "WebSocket framing is aStageofstd/web, as WEB.md proposes" (WEB.md section 9 item 5: "not in the transport"). WEB.md slice 8 (#105, which already depends on #74) owns the framing, theSec-WebSocket-Accepthandshake (Sha1exists instd/digest) and the session task. So the old criterion "WebSocket framing usable on it" moves to #105; no new issue is needed, and none is filed. Nothing else of the epic is left forstd/http: the client-side upgrade is not in slice 14 (the record names only the server), andstd/webdoes not exist yet (#94).Dependencies, verified on
main(bc504c24). None open. Slice 13 (disconnect watch) is done and shapes the design below (watchPeerinstd/http/src/server.trb);Connection(std/http/src/connection.trb) already wraps plain TCP and TLS in oneTransport, so one mechanism serveshttpandhttps. Seed impact: none (no syntax, native, driver flag or looked-up std name).Technical decisions (all inside NETWORK.md; no owner question).
std/http/src/message.trb:Status.switchingProtocols(101; the reason phrase already exists).public type Upgrade = (duplex: Duplex) => Task<Result<Void, HttpError>>(the same failure type asHandler).Responsegets a last fieldvar upgrade: Upgrade? = None(a default, so the positional constructors in the client and parser stay valid) andstatic fn upgrade(handler: Upgrade, headers: Headers = Headers()): Self, status 101, empty body, the given fields. The protocol fields (Upgrade: websocket,Connection: Upgrade,Sec-WebSocket-*) are the handler's, so the mechanism stays generic andstd/webwrites the WebSocket ones.std/http/src/duplex.trb:public shared type Duplex with Closeover the server'sConnection:fn source(chunk: Int = 65536): Source<Bytes, HttpError>,fn sink(): Sink<Bytes, HttpError>,fn remote(): SocketAddress?,fn isEncrypted(): Bool. The source first yields whatever the connection had buffered after the request head (a client may send its first frame with the request, andwatchPeermay have read bytes), then reads the transport; it ends withNonewhen the peer ends.sink().add(bytes)writes all of it,sink().end()finishes the writing half (close_notifyon TLS). Re-exported fromlib.trb(Duplex,Upgrade).std/http/src/write.trb: a newwriteUpgrade(connection, response)writesHTTP/1.1 101 <phrase>, the handler's fields (theConnectionfilter ofisFramingFieldis lifted for a 101, sinceConnection: Upgradeis the handler's; CR/LF/NUL sanitising and the token check stay), noContent-Length, noTransfer-Encoding, noConnection: close, no body.std/http/src/server.trb,serveConnection: after the handler answered, ifresponse.upgradeisSome: (a) status other than 101, or a 101 without a closure, is answered500like a failed handler (a bare 101 would leave the client waiting); (b) the unread request body is drained like for any answer, and a body that cannot be drained closes the connection without the 101; (c)writeUpgrade, then the closure runs with theDuplexand the connection task awaits it; (d) afterwardscloseGently, and the connection is never read as HTTP again. The head, idle and drain limits no longer apply to it; its timing is the closure's. A failing or cancelled closure closes only that connection and sends nothing (the failure stays private, as for a handler).serving.busy = true, soshutdown(grace)waits for it up tograceand then cancels it;close()cancels at once. The closure is cancelled with the connection task, theDuplexreleases with it.Acceptance criteria (replace those of the body)
Status.switchingProtocols,Response.upgrade(...),UpgradeandDuplexexist as above, are re-exported fromstd/http, and carry doc comments with an# Examplesblock (an echo upgrade).tests/conformance/http-upgrade.trb(with.expected,.exit,.allocations; tier B runs it in the VM and natively, byte for byte) over loopback with a rawTcpStreamclient: (1) a request withUpgrade: echoandConnection: Upgradeis answered101 Switching Protocolswith the handler's two fields and none ofContent-Length,Transfer-Encoding,Connection: close; (2) bytes sent in the same segment as the request head arrive first at the closure's source, then later bytes in order; (3) bytes written to the sink arrive at the client, both directions interleaved over at least two round trips; (4)sink().end()ends the client's read with end of stream, and the client closing ends the source withNone; (5) a plain request on a second connection is served beside the open upgrade; (6)101without a closure (Response.of(Status.switchingProtocols)) is answered500; (7)shutdown(grace)with an open upgrade waits forgraceand cancels the closure, observed by a shared flag likehttp-disconnect.trb.httpsvariant of (1) to (4) intests/conformance/https-upgrade.trb, built on the fixture ofhttps-exchange.trb; if that fixture does not carry a second program cheaply, the same cases go intohttps-exchange.trbinstead and the PR says so.http-exchange,http-wire,http-disconnect,http-workersandhttps-exchangekeep their.expectedfiles unchanged.docs/standard-library/http.mddocumentsDuplex,UpgradeandResponse.upgrade(declaration markers as forServer); NETWORK.md section 11 slice 14 becomes "Done (2026-10-24, server side; the WebSocket framing isstd/web, WEB.md slice 8, #105)", section 13 row 5 says "as built";skills/regenerated (torb docs skill docs skills;--checkis a tier A gate).torb format --checkclean on the touched files,tools/check-commits.shclean, tier A and B green in forge CI.Out of scope
WebSocket frames, masking,
Sec-WebSocket-Accept, ping/pong, close handshake,permessage-deflate(#105,std/web); a client-side upgrade; HTTP/2CONNECTand extended CONNECT (slice 12).Size
One PR, two Conventional Commits:
feat(std/http): upgrade a connection with 101 Switching Protocols and a Duplex(code, conformance programs) anddocs(std/http): the upgrade in the standard-library page, NETWORK.md and skills. Worker:std/and docs, so implementer sonnet/medium; the interplay withwatchPeerand the shutdown path deserves a careful read ofserveConnectionbefore the edit.[loop:torbscript] refine: ready, claim released, next: implement
Definition of ready met: goal and testable acceptance criteria (amended in the decision comment above), design point NETWORK.md slice 14 / section 13 request 5, no seed impact, one PR in two Conventional Commits, no open dependency. The WebSocket framing is not a second slice of this issue: it belongs to
std/web(#105, WEB.md slice 8, already blocked on #74), so no new issue was filed. Labelreadyset. Worker: refine worker (sonnet, read-only, repo atmainbc504c24), about 120k tokens by estimate, exact count not visible to the worker.[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T02:45:16Z
[loop:torbscript] implement: PR open #458 (code commit pushed: Status.switchingProtocols, Response.upgrade, Upgrade, Duplex, serveConnection upgrade path, http-upgrade and https-upgrade conformance programs); written without a local compiler, so forge CI decides; docs/skills commit follows, next: review (waiting on CI first)
[loop:torbscript] implement: checkpoint, PR #458 has both commits (code + conformance programs, docs + skills hand-synced since no torb binary runs in the worker). Forge CI has not started yet (all four checks pending after 15 min, runner queue). Not yet verified by a compiler: expect a fix round for compile errors, the predicted
.expectedof http-upgrade/https-upgrade, and thetorb docsdeclaration blocks. next: waiting (forge CI queue), then review[loop:torbscript] claim fix by session:loop-torbscript-20261024 until 2026-10-24T05:26:24Z
[loop:torbscript] fix: tier A and the playground were red for three compile errors of the uncompiled PR:
Responsehad a field and astatic fnboth namedupgrade(checker: one namespace of members; the playground rename test hit the same),http-upgrade.trbawaited inside the handler closure (the handler returns theTaskitself),https-upgrade.trbwroteTlsSettings trusted: [...]instead ofTlsSettings(trusted: [...]). Fix5316ae6c: the field is nowtakeover(message.trb, server.trb, docs/standard-library/http.md and its skill copy), both tests corrected and formatted. Checked locally with the nightly-20261023 toolchain:torb check std/httpand both conformance files clean,torb format --checkclean,check-commitsclean. Not run locally (CI only): the compiler suite (its whole-repository type check failed on the same error),docs declarations --check,doc --check, conformance runs in both modes, so another red round is possible. next: waiting (forge CI of5316ae6c)