diff options
| author | Adam Malczewski <[email protected]> | 2026-03-18 19:29:23 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-18 19:29:23 +0900 |
| commit | 448b7c255544052e9d30b76231b38a3099d5acc4 (patch) | |
| tree | 15738c8e7e5c271af59c7501501ec1752dcf4144 | |
| parent | be1f34744feeb9a8783e1158b96028210af04ecc (diff) | |
| download | webhook-forwarder-448b7c255544052e9d30b76231b38a3099d5acc4.tar.gz webhook-forwarder-448b7c255544052e9d30b76231b38a3099d5acc4.zip | |
| -rw-r--r-- | .rules/changelog/2026-03/18/03.md | 28 | ||||
| -rw-r--r-- | src/main.rs | 29 |
2 files changed, 49 insertions, 8 deletions
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 |
