From 448b7c255544052e9d30b76231b38a3099d5acc4 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 18 Mar 2026 19:29:23 +0900 Subject: fix urlencode --- .rules/changelog/2026-03/18/03.md | 28 ++++++++++++++++++++++++++++ src/main.rs | 29 +++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 .rules/changelog/2026-03/18/03.md diff --git a/.rules/changelog/2026-03/18/03.md b/.rules/changelog/2026-03/18/03.md new file mode 100644 index 0000000..ca69b5b --- /dev/null +++ b/.rules/changelog/2026-03/18/03.md @@ -0,0 +1,28 @@ +# Fix form-urlencoded webhook forwarding to Dokploy + +## Problem + +GitHub webhooks configured with `application/x-www-form-urlencoded` content type +were timing out when forwarded to Dokploy. The JSON content type worked fine. + +## Root Cause + +When converting a form-urlencoded body to JSON for the upstream request: + +1. All original headers (including `Content-Type: application/x-www-form-urlencoded` + and `Content-Length`) were copied to the upstream request builder. +2. `builder.header(Content-Type, "application/json")` **appended** a second + `Content-Type` header instead of replacing the first one. +3. The stale `Content-Length` from the original form-encoded body was forwarded, + but the extracted JSON payload is a different (smaller) size. + +Dokploy received duplicate `Content-Type` headers and an incorrect +`Content-Length`, causing it to misparse the request or hang. + +## Fix + +In `src/main.rs`, when the incoming body is `application/x-www-form-urlencoded`: + +- Skip `Content-Type` and `Content-Length` during the header copy loop. +- Set the correct `Content-Type: application/json` after extracting the payload. +- Let hyper compute `Content-Length` automatically from the actual body. diff --git a/src/main.rs b/src/main.rs index 7f00c62..f331403 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,19 +92,28 @@ async fn handle( let upstream_uri: Uri = format!("{}{}", base_url, upstream_path).parse()?; eprintln!("POST {} -> forwarding to {}", path, upstream_uri); + // Extract headers before consuming the request body + let headers = req.headers().clone(); + let is_form = content_type_is_form_urlencoded(&headers); + // Build upstream request preserving headers and body let mut builder = Request::builder() .method(Method::POST) .uri(&upstream_uri); - // Extract headers before consuming the request body - let headers = req.headers().clone(); - - // Copy headers (skip Host, it should match the upstream) + // Copy headers (skip Host; skip Content-Type and Content-Length when + // we are going to convert from form-urlencoded to JSON) for (name, value) in &headers { - if name != hyper::header::HOST { - builder = builder.header(name, value); + if name == hyper::header::HOST { + continue; } + if is_form + && (name == hyper::header::CONTENT_TYPE + || name == hyper::header::CONTENT_LENGTH) + { + continue; + } + builder = builder.header(name, value); } // Collect the incoming body @@ -112,14 +121,18 @@ async fn handle( // If the body is application/x-www-form-urlencoded (GitHub default), extract the // "payload" field and convert to application/json so Dokploy can parse it. - let body_bytes = if content_type_is_form_urlencoded(&headers) { + let body_bytes = if is_form { match extract_json_from_form(&body_bytes) { Some(json_bytes) => { eprintln!("POST {} -> converting form-urlencoded payload to JSON", path); builder = builder.header(hyper::header::CONTENT_TYPE, "application/json"); json_bytes } - None => body_bytes, + None => { + // Not convertible; restore original headers + builder = builder.header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded"); + body_bytes + } } } else { body_bytes -- cgit v1.2.3