summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-18 19:15:07 +0900
committerAdam Malczewski <[email protected]>2026-03-18 19:15:07 +0900
commit763c5e031f99085e6dace09ff391cdd0177ab5fd (patch)
tree549658701bfc313fe8b04d3773dbb7f04be59547
parent3961f4121a3774ec4c7a0d1db6dfb2870de31724 (diff)
downloadwebhook-forwarder-763c5e031f99085e6dace09ff391cdd0177ab5fd.tar.gz
webhook-forwarder-763c5e031f99085e6dace09ff391cdd0177ab5fd.zip
handle url-encoded webhooks as well
-rw-r--r--.gitignore1
-rw-r--r--Cargo.toml1
-rwxr-xr-xbin/test.sh9
-rw-r--r--src/main.rs43
4 files changed, 53 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..09a3919 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+.ask_user.md
diff --git a/Cargo.toml b/Cargo.toml
index 3b83602..8538969 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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