From 5ebae4be19fd20d67770db2bfcd23d7b5fdd96ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 23 Sep 2026 05:36:51 +0200 Subject: [PATCH] deps(turnloop): bump the turnloop family to 0.1.0-alpha.8 All 7 direct pins move in one commit -- the crates require each other at `^0.1.0-alpha.N` and Perry pins with `=`, so mixing versions fails resolution. The re-resolve moved exactly 9 crates (the 7 plus transitive turnloop-wasi-random and turnloop-zstd-decoder) and nothing else; lockstep is asserted over the whole lockfile, not eyeballed. Owner-approved publish-age override, applied as CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow on the re-resolve, since cargo has no per-package exclusion list. RELEASING.md wants the release source commit: d72fea0b4644af0b13376bf62b3b8181206d359e (turnloop release PR #109). alpha.7 -> alpha.8 is purely additive: it adds `Detached::from_listener_fd` (turnloop#108) and semver-checks rated it API-compatible. All of the work below is from alpha.6 -> alpha.7. TWO CHANGES THAT WERE NOT COMPILE ERRORS `ring` -- would have broken SCRAM-PLUS silently. In alpha.6 `ring` was an UNCONDITIONAL dependency of turnloop-tls, so Perry's `default-features = false` (present only to exclude the `turnloop` feature) still yielded ring. alpha.7 makes it optional-but-default, so the same flag newly STRIPS it. `tls_server_end_point` is `#[cfg(feature = "ring")]` and is what perry-tls-session feeds turnloop-postgres as RFC 5929 channel binding -- a wrong digest makes the driver offer PLUS and then fail the server signature -- and without ring `default_provider()` degrades to a RUNTIME "no rustls crypto provider" failure. `tls_server_end_point_hash` is NOT a drop-in: it returns WHICH hash RFC 5929 selects (`EndPointHash`), not the computed `Digest` (confirmed with the turnloop maintainer, who has corrected the release notes). So this adds `features = ["ring"]`, restoring exactly what alpha.6 gave Perry. http1 `Event::Upgrade` -- a live silent regression. In request mode alpha.7 raises `Event::Upgrade` INSTEAD of `Event::End` (CONNECT, or HTTP/1.1 Upgrade + `Connection: upgrade`). `turnloop_serve/conn.rs` already carried an `Event::Upgrade` arm written as unreachable, "kept so a later decoder that does raise it cannot fall through". alpha.7 is that decoder, so the arm went live and it did not fail to build. Left alone it would have bypassed the attached WebSocketServer precedence and `has_upgrade_listener` (#4973: no listener => serve as an ordinary request), and stopped dispatching CONNECT as a request. `Upgrade` now shares the `End` arm. WHERE THE UPSTREAM CHANGE-MAP DID NOT MATCH THIS WORKSPACE, checked not assumed: turnloop-postgres and turnloop-mysql are not dependencies here at all, so their listed changes reach nothing; `PoolConfig` and `Backend` are unused; `DecodeStep` is never constructed or destructured; and `LocalExecutor::turn` does not reach Perry -- that is turnloop-io's executor, which Perry deliberately never uses and which does not appear in the lockfile, so there is no completion leak. Flagged, deliberately not done: Perry's h2 pre-scan works around the peer's SettingsFrame and GOAWAY debug data now carried by alpha.7 -- five module/doc comments asserting the old API as current fact are corrected, the pre-scan is not, since moving three node:http2 surfaces wants its own commit and an h2spec run. `Loop::rebuild` may suit the drop-and-recreate at event_pump/agent_loop.rs. --- Cargo.lock | 36 ++++---- Cargo.toml | 55 +++++++++--- changelog.d/11083-turnloop-alpha8.md | 89 +++++++++++++++++++ .../src/server/turnloop_h2/conn.rs | 65 +++++++++++--- .../src/server/turnloop_h2/stream.rs | 4 +- .../src/server/turnloop_serve/conn.rs | 41 +++++---- .../perry-ext-mongodb/src/turnloop_io/ops.rs | 14 ++- crates/perry-ext-ws/src/turnloop_io.rs | 2 + crates/perry-http-client/src/tls.rs | 2 + crates/perry-http-client/src/transport.rs | 5 +- crates/perry-runtime/src/turnloop_net/mod.rs | 9 +- crates/perry-stdlib/src/turnloop_smtp/mod.rs | 11 +++ .../perry-stdlib/src/turnloop_tls_client.rs | 2 + crates/perry-tls-turnloop/src/lib.rs | 4 + 14 files changed, 273 insertions(+), 66 deletions(-) create mode 100644 changelog.d/11083-turnloop-alpha8.md diff --git a/Cargo.lock b/Cargo.lock index 15a90873fc..cb2788acd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9211,9 +9211,9 @@ dependencies = [ [[package]] name = "turnloop" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04fa71225fc8a61ce9707bf1d5510ebe448a3ba00d71924f3aff8a2140e33a4" +checksum = "0df7cb580227c4381eafa7e864e1196378d4ec751ad5aef4ac639e3ef38e9cff" dependencies = [ "js-sys", "libc", @@ -9225,9 +9225,9 @@ dependencies = [ [[package]] name = "turnloop-http" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55cb2b580b34765b6afe4ab260e0774a2ec8febc5d485a676580077cf14da9d" +checksum = "f987647bbe17162ee86afbdfe6d816ee1a56332fac254e0e0a7f1f1f5552d560" dependencies = [ "base64 0.22.1", "brotli", @@ -9244,9 +9244,9 @@ dependencies = [ [[package]] name = "turnloop-mongodb" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fe6910bc5e1d638f8afbe5d56633931580d058f2a453188e9f52435d32850d" +checksum = "dd10c55255831ee234ca4eb0d33a5c2a0c2c5b283c543fa2735d5b9dd8a4b62c" dependencies = [ "base64 0.22.1", "bson", @@ -9263,15 +9263,15 @@ dependencies = [ [[package]] name = "turnloop-redis" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2157260cc20c9842d385b229ff25039a19a71007655b7c9e334ac2ed7b25da98" +checksum = "146dcb82b9607638de22202aab70778b12ae28c0f702e4edbd390a89772b29ea" [[package]] name = "turnloop-smtp" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53ae9d6bec743a32deddf51eb3d48f563b7feef30faa12050a6aca68bd61ccb1" +checksum = "0a7748a6d8de3ad6cf0413e68c55efd50d52687eba0bc9cbfaf377dfa12fa05b" dependencies = [ "base64 0.22.1", "email-encoding", @@ -9280,9 +9280,9 @@ dependencies = [ [[package]] name = "turnloop-tls" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d4e17afc9c70416eaf89562ca6ab05a3a56080a78f282ed386c1b3bc1e8494" +checksum = "78914709655dfeb8ec22092a6c3f4bd04747e83c349cf66ff1a7a5b19bc9b6f8" dependencies = [ "ring", "rustls", @@ -9292,9 +9292,9 @@ dependencies = [ [[package]] name = "turnloop-wasi-random" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a5c57454c33d5733af9b9a016e055a4b9cd303a46f2b032c030faeef345d84c" +checksum = "14338681210328e1615ff532657d4afc32cae9143df43cb7eee8a5ea2b5366ac" dependencies = [ "getrandom 0.3.4", "getrandom 0.4.3", @@ -9302,9 +9302,9 @@ dependencies = [ [[package]] name = "turnloop-websocket" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fb0d3713b99cecf31c932e2a9b7f467bb8f4222bc17b85ef32ff83ca7b297" +checksum = "913e22ea69fe122a366732a63f9fd71128ea6fe3a0dc37a152621a7c1ca52988" dependencies = [ "base64 0.22.1", "bytes", @@ -9317,9 +9317,9 @@ dependencies = [ [[package]] name = "turnloop-zstd-decoder" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa8241bee6b6fa8fb9fba069a294d1f6a0f57bb24326700d01afe8b2ce79814" +checksum = "a2d8115e434249a0a11c3411c8422369113723e80298ad4e3b12a06a1eca278f" dependencies = [ "twox-hash", ] diff --git a/Cargo.toml b/Cargo.toml index 63df8388cb..f5b78ff69e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -387,17 +387,27 @@ perex = "0.1.9" # to carets, which is what lifts the forced downgrades alpha.2 imposed on libc, # tokio, redis and wasm-bindgen across this workspace. A caret here for the same # reason; the exact version is pinned by Cargo.lock, locked once with the -# owner-approved one-time publish-age override. alpha.6 carries turnloop#89 -# (ring slots allocated zeroed rather than written, which took Perry's idle RSS -# from +17.8 % to +2.9 % against the tokio arm and flipped two RSS rows to -# wins), #83 (multi-threaded accept / SO_REUSEPORT) and #85. Published -# 2026-09-17T13:24:47Z, sha256 -# d04fa71225fc8a61ce9707bf1d5510ebe448a3ba00d71924f3aff8a2140e33a4 — the +# owner-approved one-time publish-age override. alpha.7 carries turnloop#98-#106; +# alpha.8 adds only `Detached::from_listener_fd` (turnloop#108) and semver-checks +# rated it API-compatible, so every alpha.7 -> alpha.8 change is additive. The +# RELEASING.md exception wants the release source commit: +# d72fea0b4644af0b13376bf62b3b8181206d359e (turnloop release PR #109). +# What reaches Perry: `TcpOpts` gains `connect_timeout` and `PoolConfig` gains +# `max_undelivered`, so both need `..Default::default()` in a struct literal; +# `Backend::poll` takes a `Budget` (invisible here — Perry implements no +# `Backend`); and `Loop::rebuild(config)` is new. The alpha.7 change most likely +# to be mistaken for Perry's problem is `LocalExecutor::turn` retaining +# completions it did not issue: that is `turnloop-io`'s executor, which Perry +# deliberately does not use (see the P5 note below). Perry drives +# `Driver::turn(timeout, &mut Completions)`, whose body is unchanged but for the +# new `self.budget()` argument, so there is no completion-retention change on +# any path Perry takes. Published 2026-09-23T02:46:06Z, sha256 +# 35a7b66fdeab8e9365eb7dbe27edea9ed45e92e612558d301c69497f4373a60b — the # checksum Cargo.lock records, verified against the crates.io API rather than # against the publishing workflow going green. The soak skill's rule holds: # opting out is a committed, reviewable change and never a silent one, so the # override lives here in the diff and the lock pins the exact artifact. -turnloop = "0.1.0-alpha.6" +turnloop = "0.1.0-alpha.8" # turnloop P5 (DESIGN §5b, §12 "P5"): the sans-I/O protocol crates that # replace hyper's HTTP/1.1 parser and framer on Perry's server path, and # `tokio_rustls` on every TLS path the loop owns. Default features only — the @@ -406,20 +416,39 @@ turnloop = "0.1.0-alpha.6" # issue; Perry already owns a `turnloop::Loop` and routes P1/P2/P3 tokens # through it, so the codecs are driven sans-I/O over the P1 socket layer # instead (docs/turnloop/p5-report.md, "Why sans-I/O"). -turnloop-http = { version = "0.1.0-alpha.6", default-features = false } -turnloop-tls = { version = "0.1.0-alpha.6", default-features = false } +# +# `ring` is EXPLICIT here because alpha.7 made it an optional-but-default +# feature of turnloop-tls (turnloop-http's `ring` only forwards to it and has no +# ring-gated code of its own). In alpha.6 `ring` was an UNCONDITIONAL dependency +# of turnloop-tls, so `default-features = false` — which is here only to keep +# the `turnloop` feature off — still left Perry with ring. Dropping it in +# alpha.7 would have taken two things, neither of them a mere feature flag: +# `tls_server_end_point` is `#[cfg(feature = "ring")]`, and it is what +# `perry-tls-session` hands turnloop-postgres as RFC 5929 channel binding for +# SCRAM-SHA-256-PLUS, where a wrong or missing digest makes the driver offer +# PLUS and fail the server signature. The alpha.7 replacement +# `tls_server_end_point_hash` is NOT a drop-in: it returns which hash RFC 5929 +# selects (`EndPointHash`), not the computed `Digest`. And without `ring`, +# turnloop-tls's `default_provider()` stops returning ring and instead demands a +# rustls process-default provider, failing at RUNTIME ("no rustls crypto +# provider") rather than at compile time. So this line restores exactly what +# alpha.6 gave Perry; it does not opt into anything new. +turnloop-http = { version = "0.1.0-alpha.8", default-features = false } +turnloop-tls = { version = "0.1.0-alpha.8", default-features = false, features = [ + "ring", +] } # turnloop WS lane: the sans-I/O WebSocket protocol + HTTP-upgrade helpers that # replace tokio-tungstenite everywhere in the tree. Default features only -- # the `turnloop` feature pulls turnloop-io's LocalExecutor, which Perry cannot # adopt for the same reason P5 recorded (it builds its own Driver and drops # every completion it did not issue). See docs/turnloop/ws-report.md. -turnloop-websocket = { version = "0.1.0-alpha.6", default-features = false } +turnloop-websocket = { version = "0.1.0-alpha.8", default-features = false } # turnloop P6 (DESIGN §12 "P6"): the sans-I/O SMTP client and MIME builder that # replaces `lettre`'s tokio transport on the outbound mail path. Its `message` # module re-exports the same `lettre` 0.11 builder Perry's nodemailer surface # already uses, so only the transport changes # (docs/turnloop/p6-report.md). -turnloop-smtp = { version = "0.1.0-alpha.6", default-features = false } +turnloop-smtp = { version = "0.1.0-alpha.8", default-features = false } # turnloop P7 (docs/turnloop/p7-report.md): the sans-I/O database protocol # cores that replace sqlx, the `redis` crate and the `mongodb` driver on the # paths Perry's bindings own. Default features only, for the same reason as the @@ -429,8 +458,8 @@ turnloop-smtp = { version = "0.1.0-alpha.6", default-features = false } # it is the release that gave MySQL and MongoDB one deflate state per # connection, each compressed message framed as its own zlib stream; alpha.3 # re-created the state per message and is a wire-level regression to adopt. -turnloop-redis = { version = "0.1.0-alpha.6", default-features = false } -turnloop-mongodb = { version = "0.1.0-alpha.6", default-features = false } +turnloop-redis = { version = "0.1.0-alpha.8", default-features = false } +turnloop-mongodb = { version = "0.1.0-alpha.8", default-features = false } hex = "0.4" tempfile = "3" itoa = "1.0" diff --git a/changelog.d/11083-turnloop-alpha8.md b/changelog.d/11083-turnloop-alpha8.md new file mode 100644 index 0000000000..7ff35779a4 --- /dev/null +++ b/changelog.d/11083-turnloop-alpha8.md @@ -0,0 +1,89 @@ +### Changed + +- Bumped the turnloop family from `0.1.0-alpha.6` to `0.1.0-alpha.8` in + lockstep (`turnloop`, `turnloop-http`, `turnloop-tls`, `turnloop-websocket`, + `turnloop-smtp`, `turnloop-redis`, `turnloop-mongodb`, plus the transitive + `turnloop-wasi-random` and `turnloop-zstd-decoder`). These crates share one + workspace version and require each other at `^0.1.0-alpha.N`, so they can + only move together. Adopted under an owner-approved publish-age override; + `cargo`'s `min-publish-age` has no per-package exclusion list, so the + override is the recorded `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow` + re-resolve, and `Cargo.lock` pins the exact artifacts. Every direct crate's + checksum was verified against the crates.io API rather than against the + publishing workflow going green. + +alpha.7 → alpha.8 is purely additive: it adds `Detached::from_listener_fd` +(turnloop#108, Unix only) and semver-checks rated it API-compatible. Every +change described below is from alpha.6 → alpha.7, taken in the same commit +because the crates move in lockstep and Perry pins with `=`. + +Three of the changes needed more than a mechanical edit. + +**`turnloop-tls`'s `ring` became an optional-but-default feature, and Perry +asks for `default-features = false`.** In alpha.6 `ring` was an +*unconditional* dependency, so that setting — which is there only to keep the +`turnloop` feature (and its `turnloop-io` `LocalExecutor`) out — still left +Perry with ring. In alpha.7 it would have silently removed it, taking two +things with it. `turnloop_tls::tls_server_end_point` is `#[cfg(feature = +"ring")]`, and it is what `perry-tls-session` hands `turnloop-postgres` as RFC +5929 `tls-server-end-point` channel binding for SCRAM-SHA-256-PLUS, where a +missing or wrong digest makes the driver offer PLUS and then fail the server +signature. The alpha.7 `tls_server_end_point_hash` is **not** a drop-in +replacement: it returns *which* hash RFC 5929 §4.1 selects (`EndPointHash`), +not the computed `Digest`, so a caller must hash the leaf itself. Separately, +without `ring` the crate's `default_provider()` stops returning ring and +instead demands a rustls process-default provider, failing at **runtime** with +"no rustls crypto provider" rather than at compile time. The workspace pin now +names `features = ["ring"]` explicitly, which restores exactly what alpha.6 +gave Perry and opts into nothing new. (`turnloop-http`'s new `ring` default +only forwards to `turnloop-tls` and has no ring-gated code of its own.) + +**The http1 request-mode decoder now ends an upgrade with `Event::Upgrade` +instead of `Event::End`.** `turnloop_serve`'s connection loop already carried +an `Event::Upgrade` arm, written as unreachable ("kept so a later decoder that +does raise it cannot fall through") that routed straight to `Step::Upgrade`. +alpha.7 is that later decoder, so the arm went live — and because it already +existed, nothing about this was a compile error. Left alone it would have +bypassed both the attached-`WebSocketServer` precedence and the +`has_upgrade_listener` test (#4973: an upgrade with no listener is served as an +ordinary request), and a `CONNECT` — which sets the decoder's +`upgrade_request` but is not an `Upgrade` header, so never sets +`Building::upgrade` — would have stopped being dispatched as a request at all. +`Upgrade` now shares the `End` arm, so one copy of the routing policy serves +both. The stale doc comment on `Building::upgrade` that asserted the decoder +never raises it was corrected. + +**`LocalExecutor::turn` no longer dropping foreign completions does not reach +Perry.** That change is `turnloop-io`'s executor, which Perry deliberately does +not use — it owns its own `turnloop::Loop` and routes tokens through it, for +exactly the reason the change fixes. Perry drives +`Driver::turn(timeout, &mut Completions)`, whose body is identical between +alpha.6 and alpha.7 apart from the new internal `self.budget()` argument to +`Backend::poll`. There is no completion-retention behaviour change on any path +Perry takes, and no completion leak. + +The mechanical edits: `TcpOpts` and `ClientOptions` struct literals gained the +new `connect_timeout` / `provider` fields (both left at the default, which is +alpha.6's behaviour — no connect timeout, and the ring provider); the http2 +`Event::Settings(..)` / `Event::Goaway { .. }` patterns were updated and the +new `Event::SettingsAck` is carried and ignored, matching the silent step it +replaced; and `turnloop-smtp`'s new `Event::BodyReady` got an arm documenting +that it cannot arrive here, because this module only ever sends whole messages +and never starts a streamed one. + +Noted and deliberately **not** acted on, since a dependency bump is the wrong +place to add behaviour: alpha.7's `Event::Settings` now carries the peer's +actual `SettingsFrame` and `Event::Goaway` its RFC 9113 §6.8 debug data, both +of which this tree currently works around or drops; `turnloop-mongodb`'s +`WriteResult::parse` is now strict about `writeErrors` / `writeConcernError` +(the lenient parse moved to `WriteResult::decode`), which is exactly the +`Error::from_response` check `perry-ext-mongodb` already ran immediately before +each call, so the calls are now redundant rather than load-bearing and the +comment saying otherwise was corrected; and `turnloop`'s new `Loop::rebuild` +may suit `event_pump/agent_loop.rs`'s profile upgrade, which currently drops +and recreates a loop. + +`turnloop-postgres` and `turnloop-mysql` are not dependencies of this +workspace, so the alpha.7 changes to them (`Error::Transport`, +`Value::Unknown`, `consume_output`, `Event::Eof`, TIME-as-String) reach nothing +here. diff --git a/crates/perry-ext-http/src/server/turnloop_h2/conn.rs b/crates/perry-ext-http/src/server/turnloop_h2/conn.rs index 01e8604fc0..8932428316 100644 --- a/crates/perry-ext-http/src/server/turnloop_h2/conn.rs +++ b/crates/perry-ext-http/src/server/turnloop_h2/conn.rs @@ -32,19 +32,29 @@ //! into `core.output()`. Returning without flushing sends a peer nothing at //! all, and h2spec asks for that frame by error code on ~60 of its tests. //! -//! ## The pre-scan, and the three things the core will not tell you +//! ## The pre-scan, and the three things it recovers //! //! Every frame is decoded **twice**: once by [`peek_frame`] here, once by the //! core. The second decode is the authoritative one; the first exists because -//! three facts a `node:http2` session has to surface never leave `Connection`: +//! three facts a `node:http2` session has to surface did not leave +//! `Connection` when this was written: //! //! * **a SETTINGS acknowledgement** — consumed with `event: None`, so -//! `session.settings(obj, cb)` has nothing to fire its callback on; -//! * **GOAWAY's opaque data** — `Event::Goaway` carries `last_stream` and -//! `code` only, and Node's `'goaway'` listener receives the third argument; -//! * **the peer's SETTINGS values** — `Event::Settings` is a unit variant, so +//! `session.settings(obj, cb)` had nothing to fire its callback on; +//! * **GOAWAY's opaque data** — `Event::Goaway` carried `last_stream` and +//! `code` only, and Node's `'goaway'` listener receives a third argument; +//! * **the peer's SETTINGS values** — `Event::Settings` was a unit variant, so //! `session.remoteSettings` would stay at its defaults forever. //! +//! **turnloop-http 0.1.0-alpha.7 now carries all three** — `Event::SettingsAck` +//! is an event, `Event::Goaway` has a `debug` field, and `Event::Settings` +//! carries a `SettingsFrame`. The pre-scan was deliberately left in place at +//! that bump rather than unwound: it is also what withholds the frame class +//! described next, and moving three `node:http2`-visible surfaces onto a new +//! source is a behaviour change that wants its own commit and its own h2spec +//! run. Treat the list above as the reason the pre-scan EXISTS, not as a +//! current statement about the core's API. +//! //! The pre-scan also *withholds* one frame class from the core. `Connection` //! tracks exactly one outstanding SETTINGS (its own, from the constructor) and //! answers a second acknowledgement with `protocol("unsolicited SETTINGS @@ -109,12 +119,25 @@ pub(crate) enum Owned { /// exhaustive and the decision is visible; answering it would diverge from /// Node, which sends nothing. Unprocessed, + /// The peer acknowledged our SETTINGS. Before turnloop-http 0.1.0-alpha.7 + /// this was a silent step inside the decoder rather than an event, so + /// nothing here ever observed it; it is carried (and ignored) to keep that + /// behaviour explicit rather than to start acting on it. The round-trip + /// timing the ack now makes measurable is unused — `session.ping()` is the + /// surface Node exposes for that. + SettingsAck, WindowUpdate, } fn own_event(event: Event<'_>) -> Owned { match event { - Event::Settings => Owned::Settings, + // alpha.7 gave `Settings` the peer's actual parameters + // (`SettingsFrame`). Still discarded here: `session.remoteSettings` is + // served from `stream::on_peer_settings`'s own view of the applied + // settings, so reading them off the frame would be a second source of + // the same truth. See the `remoteSettings` note below. + Event::Settings(_) => Owned::Settings, + Event::SettingsAck(_) => Owned::SettingsAck, Event::Headers { stream, headers, @@ -152,7 +175,13 @@ fn own_event(event: Event<'_>) -> Owned { // and measurement against a raw peer showed it does not. Event::Unprocessed { .. } => Owned::Unprocessed, Event::Reset { stream, code } => Owned::Reset { stream, code }, - Event::Goaway { last_stream, code } => Owned::Goaway { last_stream, code }, + // `..` drops alpha.7's new `debug` (RFC 9113 §6.8 Additional Debug + // Data). Node surfaces it as the `'goaway'` handler's third argument; + // wiring it through is a behaviour ADDITION, so it is deliberately not + // done in a dependency bump. See the `Additional Debug Data` note below. + Event::Goaway { + last_stream, code, .. + } => Owned::Goaway { last_stream, code }, Event::Ping { ack, data } => Owned::Ping { ack, data }, Event::WindowUpdate { .. } => Owned::WindowUpdate, } @@ -221,11 +250,14 @@ pub(crate) struct H2Conn { /// SETTINGS frames this module wrote out of band (`session.settings()`) /// whose acknowledgement has not arrived. See the module docs. pub(crate) owed_settings_acks: u32, - /// GOAWAY opaque data captured by the pre-scan, for the `'goaway'` event - /// the core's `Event::Goaway` cannot carry. + /// GOAWAY opaque data captured by the pre-scan, for the `'goaway'` event. + /// `Event::Goaway` gained a `debug` field in turnloop-http alpha.7; this + /// remains the source (see the module docs' pre-scan note). pub(crate) goaway_opaque: Vec, /// The peer's SETTINGS values captured by the pre-scan, for the - /// `'remoteSettings'` event the core's unit `Event::Settings` cannot carry. + /// `'remoteSettings'` event. `Event::Settings` carries a `SettingsFrame` + /// since turnloop-http alpha.7; this remains the source (see the module + /// docs' pre-scan note). pub(crate) peer_settings: Option, /// Connection-level frames JS asked for before the transport was ready. /// @@ -1041,6 +1073,10 @@ fn apply(conn: &mut H2Conn, event: Owned) { // Deliberately nothing: see `Event::Unprocessed` above. Node sends no // frame and never surfaces the request, and so do we. Owned::Unprocessed => {} + // Deliberately nothing: turnloop-http applies the acknowledged settings + // itself, and before alpha.7 made this an event the decoder stepped + // over it silently. Ignoring it keeps that exact behaviour. + Owned::SettingsAck => {} // A peer window opened: retry whatever stalled. Which window — the // connection's or one stream's — does not matter, because // `pump_outbox` walks every stream and `send_data` answers zero for @@ -1420,8 +1456,8 @@ mod prescan_tests { assert_eq!(c.owed_settings_acks, 1); } - /// `Event::Settings` is a unit variant, so `session.remoteSettings` comes - /// from here or from nowhere. + /// `session.remoteSettings` comes from the pre-scan, not from the core's + /// `Event::Settings` (see the module docs' pre-scan note). #[test] fn peer_settings_values_are_captured() { let mut c = conn(0, true); @@ -1443,7 +1479,8 @@ mod prescan_tests { assert_eq!(c.input.len(), 9 + payload.len()); } - /// RFC 9113 §6.8's Additional Debug Data, which `Event::Goaway` drops. + /// RFC 9113 §6.8's Additional Debug Data, which this module takes from the + /// pre-scan rather than from `Event::Goaway`'s alpha.7 `debug` field. #[test] fn goaway_opaque_data_is_captured_and_cleared() { let mut c = conn(0, true); diff --git a/crates/perry-ext-http/src/server/turnloop_h2/stream.rs b/crates/perry-ext-http/src/server/turnloop_h2/stream.rs index e277c1b3df..fc13ed56b5 100644 --- a/crates/perry-ext-http/src/server/turnloop_h2/stream.rs +++ b/crates/perry-ext-http/src/server/turnloop_h2/stream.rs @@ -164,8 +164,8 @@ pub(crate) fn on_peer_settings(conn: &mut H2Conn) { if session == 0 { return; } - // `Event::Settings` is a unit variant, so the values came from the - // pre-scan in `conn.rs` rather than from the core. + // The values come from the pre-scan in `conn.rs` rather than from the + // core's `Event::Settings` (see conn.rs's module docs). let settings = conn.peer_settings.take().unwrap_or_default(); crate::server::http2_server::queue_turnloop_remote_settings(session, settings); } diff --git a/crates/perry-ext-http/src/server/turnloop_serve/conn.rs b/crates/perry-ext-http/src/server/turnloop_serve/conn.rs index ebdc5498cb..ddc790a9ba 100644 --- a/crates/perry-ext-http/src/server/turnloop_serve/conn.rs +++ b/crates/perry-ext-http/src/server/turnloop_serve/conn.rs @@ -38,11 +38,13 @@ struct Building { /// `Connection: upgrade` with an `Upgrade` header — Node dispatches this /// to `'upgrade'` rather than `'request'`, *if* a listener exists. /// - /// The decoder does not raise `Event::Upgrade` for it: - /// `turnloop_http::http1`'s `State::Upgrade` is only reachable in - /// `Mode::Response` (a client reading a 101), so on the request side an - /// upgrade is an ordinary head with no body and the server is the one that - /// has to recognize it. + /// Recognized here, from the head, rather than taken from the decoder. + /// Since turnloop-http 0.1.0-alpha.7 the request-mode decoder DOES raise + /// `Event::Upgrade` (it used to be reachable only in `Mode::Response`, a + /// client reading a 101), but that event is routed into the same arm as + /// `Event::End` and the routing still keys off this flag — the two do not + /// agree on CONNECT, which sets the decoder's `upgrade_request` but is not + /// an `Upgrade` header and so is not one of these. upgrade: bool, /// `Connection: upgrade` naming `websocket`, with a `Sec-WebSocket-Key`. /// An attached `WebSocketServer` answers these itself. @@ -446,7 +448,25 @@ fn decode(id: i64) { outcome = Step::Again; } Some(http1::Event::Trailers(_)) => outcome = Step::Again, - Some(http1::Event::End) => { + // `Upgrade` joins `End` here rather than getting its own arm. + // turnloop-http 0.1.0-alpha.7 made the REQUEST-mode decoder end + // an upgrade message with `Event::Upgrade` INSTEAD of + // `Event::End` (`upgrade_request` = CONNECT, or HTTP/1.1 with + // `Upgrade` + `Connection: upgrade`). Both mean the same thing + // on this side — the message is complete — and all the routing + // policy lives below, so they must not diverge. + // + // This was a silent regression waiting to happen: the old + // `Event::Upgrade` arm was written as unreachable and routed + // straight to `Step::Upgrade`, so once alpha.7 started raising + // it, every upgrade would have bypassed BOTH the attached + // `WebSocketServer` precedence and the `has_upgrade_listener` + // test (#4973: an upgrade with no listener is served as an + // ordinary request), and a CONNECT — which sets + // `upgrade_request` but never `Building::upgrade` — would have + // stopped being dispatched as a request at all. None of that is + // a compile error, because the arm already existed. + Some(http1::Event::End | http1::Event::Upgrade) => { outcome = match c.building.take() { // A WebSocket upgrade with a `WebSocketServer` attached // to this server is answered here, before the generic @@ -479,15 +499,6 @@ fn decode(id: i64) { None => Step::Again, }; } - // Unreachable on the request side (see `Building::upgrade`), - // and handled above when it is; kept so a later decoder that - // does raise it cannot fall through to "needs more input". - Some(http1::Event::Upgrade) => { - outcome = match c.building.take() { - Some(building) => Step::Upgrade(building), - None => Step::Idle, - }; - } Some(http1::Event::Informational(_)) => outcome = Step::Again, None => { if consumed > 0 { diff --git a/crates/perry-ext-mongodb/src/turnloop_io/ops.rs b/crates/perry-ext-mongodb/src/turnloop_io/ops.rs index fa77669204..8cdd717e83 100644 --- a/crates/perry-ext-mongodb/src/turnloop_io/ops.rs +++ b/crates/perry-ext-mongodb/src/turnloop_io/ops.rs @@ -347,10 +347,20 @@ impl Operation { ))) } Kind::InsertOne { id } => { - // `WriteResult::parse` only reports a failure when `ok` is 0. // A duplicate key answers `ok: 1` with a `writeErrors` array, // and the pre-P7 path rejected on that, so ask the response - // parser that knows about write errors first. + // parser that knows about write errors. This kind settles a + // plain id and so never builds a `WriteResult` at all. + // + // (Before turnloop-mongodb 0.1.0-alpha.7 this was the ONLY + // check that caught a write error: `WriteResult::parse` then + // failed only on `ok: 0`. alpha.7 made `parse` run exactly this + // `Error::from_response` first — the lenient parse is now + // `WriteResult::decode` — so the explicit calls at the kinds + // below are redundant rather than load-bearing. They are kept: + // they cost one pass over a reply that is already in memory, + // they produce the identical error, and they keep every kind + // here rejecting write errors the same visible way.) Error::from_response(reply).map_err(message)?; Ok(Step::Settle(Settlement::Json(std::mem::take(id)))) } diff --git a/crates/perry-ext-ws/src/turnloop_io.rs b/crates/perry-ext-ws/src/turnloop_io.rs index ffb0ad8688..93dedbf519 100644 --- a/crates/perry-ext-ws/src/turnloop_io.rs +++ b/crates/perry-ext-ws/src/turnloop_io.rs @@ -595,6 +595,8 @@ fn tls_config() -> Result<&'static turnloop_tls::ClientConfig, String> { extra_ca_pem, reject_unauthorized: !environment.accepts_invalid_certificates(), enable_sni: true, + // `None` = turnloop-tls's default provider, `ring`. + provider: None, }; turnloop_tls::ClientConfig::new(options, unix_seconds()).map_err(|e| e.to_string()) }) diff --git a/crates/perry-http-client/src/tls.rs b/crates/perry-http-client/src/tls.rs index 7a9511f820..ebf8cd8421 100644 --- a/crates/perry-http-client/src/tls.rs +++ b/crates/perry-http-client/src/tls.rs @@ -51,6 +51,8 @@ pub fn client_config() -> Result<&'static turnloop_tls::ClientConfig> { // Never configurable from this crate — see the module docs. reject_unauthorized: true, enable_sni: true, + // `None` = turnloop-tls's default provider, `ring`. + provider: None, }; turnloop_tls::ClientConfig::new(options, unix_seconds()).map_err(|e| e.to_string()) }) diff --git a/crates/perry-http-client/src/transport.rs b/crates/perry-http-client/src/transport.rs index 72f1a51bd9..536a44e2d8 100644 --- a/crates/perry-http-client/src/transport.rs +++ b/crates/perry-http-client/src/transport.rs @@ -133,7 +133,10 @@ impl Connection { }; let mut driver = Loop::new(config).map_err(turnloop_io_error)?; let deadline = driver.now() + budget; - let opts = TcpOpts { nodelay: true }; + let opts = TcpOpts { + nodelay: true, + ..TcpOpts::default() + }; let handle = driver .tcp_connect(addr, &opts, TOK_CONNECT) .map_err(turnloop_io_error)?; diff --git a/crates/perry-runtime/src/turnloop_net/mod.rs b/crates/perry-runtime/src/turnloop_net/mod.rs index 838ad3eb34..03e98eb17d 100644 --- a/crates/perry-runtime/src/turnloop_net/mod.rs +++ b/crates/perry-runtime/src/turnloop_net/mod.rs @@ -483,7 +483,14 @@ pub fn accept_start(id: i64) -> NetResult<()> { pub fn tcp_connect(id: i64, subsystem: u8, addr: SocketAddr, nodelay: bool) -> NetResult<()> { with_driver(|driver| { let handle = driver - .tcp_connect(addr, &TcpOpts { nodelay }, token(OP_CONNECT, id)) + .tcp_connect( + addr, + &TcpOpts { + nodelay, + ..TcpOpts::default() + }, + token(OP_CONNECT, id), + ) .map_err(|e| map_error(e, "connect"))?; let mut entry = Entry::new(handle, subsystem, false); entry.peer = Some(addr); diff --git a/crates/perry-stdlib/src/turnloop_smtp/mod.rs b/crates/perry-stdlib/src/turnloop_smtp/mod.rs index 41c26f6136..d18cc94cd9 100644 --- a/crates/perry-stdlib/src/turnloop_smtp/mod.rs +++ b/crates/perry-stdlib/src/turnloop_smtp/mod.rs @@ -595,6 +595,17 @@ fn pump(state: &mut EngineState, id: i64) { return; } Some(Event::Reset) => {} + // turnloop-smtp 0.1.0-alpha.7 added this for STREAMED messages: + // the core pushes it only when `streaming.is_some()`, which is set + // only by the streaming starter. This module sends whole messages + // (`.send(..)` below) and never starts one, so the event cannot + // arrive and there is nothing to write. + // + // It is not merely ignorable, though: it means the server answered + // DATA with 354 and is waiting for content, so a future streaming + // path that leaves this arm empty would hang the exchange rather + // than fail it. Whoever adds streaming must handle it here. + Some(Event::BodyReady { .. }) => {} None => { flush(state, id); return; diff --git a/crates/perry-stdlib/src/turnloop_tls_client.rs b/crates/perry-stdlib/src/turnloop_tls_client.rs index da69092a4f..9a9d7ff620 100644 --- a/crates/perry-stdlib/src/turnloop_tls_client.rs +++ b/crates/perry-stdlib/src/turnloop_tls_client.rs @@ -53,6 +53,8 @@ pub(crate) fn client_config() -> Option<&'static turnloop_tls::ClientConfig> { extra_ca_pem, reject_unauthorized: !environment.accepts_invalid_certificates(), enable_sni: true, + // `None` = turnloop-tls's default provider, `ring`. + provider: None, }; turnloop_tls::ClientConfig::new(options, unix_seconds()).ok() }) diff --git a/crates/perry-tls-turnloop/src/lib.rs b/crates/perry-tls-turnloop/src/lib.rs index 150869d850..e2755099c3 100644 --- a/crates/perry-tls-turnloop/src/lib.rs +++ b/crates/perry-tls-turnloop/src/lib.rs @@ -311,6 +311,10 @@ pub fn client_config(options: &TlsClientOptions) -> Result