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