From 763c5e031f99085e6dace09ff391cdd0177ab5fd Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 18 Mar 2026 19:15:07 +0900 Subject: handle url-encoded webhooks as well --- src/main.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) (limited to 'src') 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 { 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=` when the webhook +/// content type is set to `application/x-www-form-urlencoded`. +fn extract_json_from_form(body: &Bytes) -> Option { + 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, @@ -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 -- cgit v1.2.3