diff options
| author | Adam Malczewski <[email protected]> | 2026-03-18 19:15:07 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-18 19:15:07 +0900 |
| commit | 763c5e031f99085e6dace09ff391cdd0177ab5fd (patch) | |
| tree | 549658701bfc313fe8b04d3773dbb7f04be59547 | |
| parent | 3961f4121a3774ec4c7a0d1db6dfb2870de31724 (diff) | |
| download | webhook-forwarder-763c5e031f99085e6dace09ff391cdd0177ab5fd.tar.gz webhook-forwarder-763c5e031f99085e6dace09ff391cdd0177ab5fd.zip | |
handle url-encoded webhooks as well
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rwxr-xr-x | bin/test.sh | 9 | ||||
| -rw-r--r-- | src/main.rs | 43 |
4 files changed, 53 insertions, 1 deletions
@@ -1 +1,2 @@ /target +.ask_user.md @@ -9,3 +9,4 @@ http-body-util = "0.1.3" hyper = { version = "1.8.1", features = ["server", "client", "http1"] } hyper-util = { version = "0.1.20", features = ["tokio", "client-legacy", "http1"] } tokio = { version = "1.50.0", features = ["rt-multi-thread", "net", "macros"] } +form_urlencoded = "1" diff --git a/bin/test.sh b/bin/test.sh index ca305aa..b8294de 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -32,4 +32,13 @@ STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/foo/bar/baz") echo "POST /foo/bar/baz -> $STATUS (expect 404)" echo "" +echo "=== Test 6: POST form-urlencoded payload should be converted to JSON ===" +PAYLOAD=$(python3 -c "import urllib.parse; print(urllib.parse.urlencode({'payload': '{\"ref\":\"refs/heads/main\"}'}))") +RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE/test-token" -H "Content-Type: application/x-www-form-urlencoded" -H "X-GitHub-Event: push" -d "$PAYLOAD") +STATUS=$(echo "$RESP" | tail -1) +BODY=$(echo "$RESP" | head -n -1) +echo "POST /test-token (form-urlencoded) -> $STATUS (expect 502 if Dokploy unreachable, or upstream status)" +echo "Body: $BODY" + +echo "" echo "Done!" diff --git a/src/main.rs b/src/main.rs index 1f336ae..7f00c62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use hyper::body::Incoming; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Method, Request, Response, StatusCode, Uri}; +use hyper::header::HeaderMap; use hyper_util::client::legacy::Client; use hyper_util::rt::TokioExecutor; use tokio::net::TcpListener; @@ -40,6 +41,27 @@ fn map_path(path: &str) -> Option<String> { None } +/// Check if the Content-Type header indicates form-urlencoded data. +fn content_type_is_form_urlencoded(headers: &HeaderMap) -> bool { + headers + .get(hyper::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .map(|ct| ct.starts_with("application/x-www-form-urlencoded")) + .unwrap_or(false) +} + +/// Extract JSON from a form-urlencoded body. +/// GitHub sends webhooks as `payload=<url-encoded-json>` when the webhook +/// content type is set to `application/x-www-form-urlencoded`. +fn extract_json_from_form(body: &Bytes) -> Option<Bytes> { + for (key, value) in form_urlencoded::parse(body) { + if key == "payload" { + return Some(Bytes::from(value.into_owned())); + } + } + None +} + async fn handle( base_url: String, req: Request<Incoming>, @@ -75,8 +97,11 @@ async fn handle( .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) - for (name, value) in req.headers() { + for (name, value) in &headers { if name != hyper::header::HOST { builder = builder.header(name, value); } @@ -84,6 +109,22 @@ async fn handle( // Collect the incoming body let body_bytes = req.into_body().collect().await?.to_bytes(); + + // 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) { + 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, + } + } else { + body_bytes + }; + let upstream_req = builder.body(Full::new(body_bytes))?; // Send to Dokploy |
