From 4266341733ae6f773b0b51d5daf0b0b101e35d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Thu, 19 Mar 2026 10:17:18 +0100 Subject: [PATCH] fix(worker): ensure that requests body are proxied We were seeing body for DELETE requests not being proxied correctly when going through the worker deployed on Cloudflare. The root seems to be on the TryFrom for the worker::Request ignoring the body on DELETE requests on the worker. Now we are constructing the worker Request manually to ensure the body is present. --- worker/src/lib.rs | 77 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 83c8ec1c..8d12e559 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -1,4 +1,5 @@ use axum::{ + body::to_bytes, extract::{Json, Query, Request, State}, http::StatusCode, middleware::{from_fn_with_state, Next}, @@ -17,7 +18,7 @@ use serde::{Deserialize, Serialize}; use tower_service::Service; use worker::{ console_error, console_log, console_warn, event, kv::KvStore, Env, Fetch, HttpRequest, - HttpResponse, + HttpResponse, RequestRedirect, }; use ws::handle_ws_resp; @@ -330,15 +331,9 @@ async fn linkup_request_handler( req.headers_mut().remove(http::header::HOST); linkup::normalize_cookie_header(req.headers_mut()); - let upstream_request: worker::Request = match req.try_into() { - Ok(req) => req, - Err(e) => { - return HttpError::new( - format!("Failed to parse request: {}", e), - StatusCode::BAD_REQUEST, - ) - .into_response() - } + let upstream_request = match convert_request(req).await { + Ok(worker_request) => worker_request, + Err(error) => return error.into_response(), }; let cacheable_req = is_cacheable_request(&upstream_request, &config); @@ -366,7 +361,7 @@ async fn linkup_request_handler( format!("Failed to fetch from target service: {}", e), StatusCode::BAD_GATEWAY, ) - .into_response() + .into_response(); } }; @@ -396,6 +391,66 @@ async fn linkup_request_handler( } } +// NOTE(augustoccesar)[2026-03-19]: The reason to build this manually instead of using the TryFrom for +// the worker::Request is because of how body is constructed. +// We were seeing body for DELETE requests not being proxied correctly. Because of that we are now +// doing the reconstruct manually to ensure the body is present. +// +// This is not ideal and would be great to keep trusting the `to_wasm` from the workers-rs, +// but for now this solves our issue. +// Main concern here is if we might be missing some of the other internal operations that are +// done during the conversion. But for our usecases, it seems to be working. +async fn convert_request( + req: http::Request, +) -> Result { + const MAX_BODY_SIZE: usize = 100 * 1024 * 1024; // 100MB, same as local-server limit + + let (parts, body) = req.into_parts(); + let body_bytes = match to_bytes(body, MAX_BODY_SIZE).await { + Ok(bytes) => bytes, + Err(e) => { + return Err(HttpError::new( + format!("Failed to extract request body: {}", e), + StatusCode::BAD_REQUEST, + )); + } + }; + + let target_url = parts.uri.to_string(); + + let mut headers = worker::Headers::new(); + for (name, value) in parts.headers.iter() { + if let Ok(header_value) = value.to_str() { + let _ = headers.set(name.as_str(), header_value); + } + } + + let mut request_init = worker::RequestInit::new(); + request_init + .with_method(worker::Method::from(parts.method.to_string())) + .with_headers(headers) + .with_redirect(RequestRedirect::Manual); + + if !body_bytes.is_empty() { + request_init.with_body(Some(wasm_bindgen::JsValue::from_str( + &String::from_utf8_lossy(&body_bytes), + ))); + } + + let worker_request: worker::Request = + match worker::Request::new_with_init(&target_url, &request_init) { + Ok(req) => req, + Err(e) => { + return Err(HttpError::new( + format!("Failed to create request: {}", e), + StatusCode::BAD_REQUEST, + )); + } + }; + + Ok(worker_request) +} + async fn cleanup_unused_sessions(state: &LinkupState) { let tunnels_keys = state .tunnels_kv