diff --git a/Cargo.lock b/Cargo.lock index 983e46d..f4453cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,7 +1314,7 @@ dependencies = [ [[package]] name = "multistore" -version = "0.7.1" +version = "0.7.2" dependencies = [ "async-trait", "base64", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "multistore-cf-workers" -version = "0.7.1" +version = "0.7.2" dependencies = [ "async-trait", "bytes", @@ -1366,7 +1366,7 @@ dependencies = [ [[package]] name = "multistore-cf-workers-example" -version = "0.7.1" +version = "0.7.2" dependencies = [ "bytes", "console_error_panic_hook", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "multistore-lambda" -version = "0.7.1" +version = "0.7.2" dependencies = [ "bytes", "http", @@ -1411,7 +1411,7 @@ dependencies = [ [[package]] name = "multistore-metering" -version = "0.7.1" +version = "0.7.2" dependencies = [ "bytes", "futures", @@ -1423,7 +1423,7 @@ dependencies = [ [[package]] name = "multistore-oidc-provider" -version = "0.7.1" +version = "0.7.2" dependencies = [ "base64", "chrono", @@ -1442,7 +1442,7 @@ dependencies = [ [[package]] name = "multistore-path-mapping" -version = "0.7.1" +version = "0.7.2" dependencies = [ "multistore", "percent-encoding", @@ -1451,7 +1451,7 @@ dependencies = [ [[package]] name = "multistore-server" -version = "0.7.1" +version = "0.7.2" dependencies = [ "axum", "bytes", @@ -1473,7 +1473,7 @@ dependencies = [ [[package]] name = "multistore-static-config" -version = "0.7.1" +version = "0.7.2" dependencies = [ "chrono", "multistore", @@ -1485,7 +1485,7 @@ dependencies = [ [[package]] name = "multistore-sts" -version = "0.7.1" +version = "0.7.2" dependencies = [ "aes-gcm", "base64", diff --git a/crates/cf-workers/src/backend.rs b/crates/cf-workers/src/backend.rs index e54e67a..64f41e3 100644 --- a/crates/cf-workers/src/backend.rs +++ b/crates/cf-workers/src/backend.rs @@ -60,6 +60,32 @@ fn new_fixed_length_stream( } } +/// Observe the request-body pipe so its failure reason reaches the logs instead +/// of being discarded. +/// +/// Spawns rather than awaits: `forward` must return as soon as the outbound +/// response headers arrive, and the pipe is still driven by that fetch's +/// backpressure. The spawned task only observes a promise — it performs no I/O +/// of its own and does not extend the request's lifetime. +/// +/// The rejection is the only place the real cause is visible — the inbound body +/// ending short of, or running past, the declared `Content-Length` (either errors +/// the `FixedLengthStream`), the inbound stream itself erroring, or the outbound +/// fetch cancelling `readable`. Downstream all of these look identical: a +/// truncated subrequest body and an origin that hangs up without responding. +fn log_pipe_rejection(pipe: js_sys::Promise, declared_len: u64) { + wasm_bindgen_futures::spawn_local(async move { + if let Err(err) = wasm_bindgen_futures::JsFuture::from(pipe).await { + tracing::warn!( + declared_content_length = declared_len, + error = ?err, + "request body pipe failed; the outbound body is truncated and the \ + origin will likely close without responding" + ); + } + }); +} + impl ProxyBackend for WorkerBackend { type ResponseBody = web_sys::Response; type Body = JsBody; @@ -104,13 +130,18 @@ impl ProxyBackend for WorkerBackend { let transform: &web_sys::TransformStream = fls.as_ref(); // The outbound fetch consuming `readable` pulls the body // through the transform; the pipe is driven by that - // backpressure, so it streams rather than buffers. The - // returned promise is intentionally dropped: a pipe - // failure (e.g. the client sending fewer/more bytes than - // Content-Length, which errors the FixedLengthStream) - // also errors `readable`, so the awaited outbound fetch - // fails and the error surfaces there. - let _ = stream.pipe_to(&transform.writable()); + // backpressure, so it streams rather than buffers. + // + // The pipe's rejection is logged rather than discarded. + // It used to be dropped on the reasoning that a pipe + // failure also errors `readable`, so the awaited outbound + // fetch would surface it. In practice it does not: the + // origin sees a truncated request, closes without + // replying, and the runtime turns that into a + // Cloudflare-minted `error code: 520` carrying no + // diagnostic — no origin request id, no error document. + // The reason the body pipe failed is then unrecoverable. + log_pipe_rejection(stream.pipe_to(&transform.writable()), len); init.set_body(&transform.readable()); } None => init.set_body(stream),