Skip to content

Commit cf221e9

Browse files
vdavezclaude
andcommitted
Receiver: return JSON for 401/404 instead of stdlib HTML
The default BaseHTTPRequestHandler error page is HTML, which makes the receiver's rejection responses inconsistent with tango's own JSON-shaped API and harder to inspect programmatically (e.g. via simulate's response_body field). Now responds with `{"ok": false, "error": "<code>"}` and Content-Type application/json on 401 (invalid_signature) and 404 (not_found), and `{"ok": true}` on 200 — explicit Content-Length on all paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e049bd commit cf221e9

2 files changed

Lines changed: 15 additions & 4 deletions

File tree

tango/webhooks/receiver.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def log_message(self, format: str, *args: Any) -> None: # noqa: A002
114114

115115
def do_POST(self) -> None: # noqa: N802 (stdlib API)
116116
if self.path != receiver.path:
117-
self.send_error(404, "Not Found")
117+
self._write_json(404, {"ok": False, "error": "not_found"})
118118
return
119119
length = int(self.headers.get("Content-Length", "0") or 0)
120120
body = self.rfile.read(length) if length > 0 else b""
@@ -130,7 +130,7 @@ def do_POST(self) -> None: # noqa: N802 (stdlib API)
130130
)
131131
if require and not verified:
132132
self._record(body, signature, verified=False)
133-
self.send_error(401, "Invalid signature")
133+
self._write_json(401, {"ok": False, "error": "invalid_signature"})
134134
return
135135

136136
forward_status: int | None = None
@@ -145,10 +145,15 @@ def do_POST(self) -> None: # noqa: N802 (stdlib API)
145145
forward_status=forward_status,
146146
forward_error=forward_error,
147147
)
148-
self.send_response(200)
148+
self._write_json(200, {"ok": True})
149+
150+
def _write_json(self, status: int, body: dict[str, Any]) -> None:
151+
payload = json.dumps(body).encode("utf-8")
152+
self.send_response(status)
149153
self.send_header("Content-Type", "application/json")
154+
self.send_header("Content-Length", str(len(payload)))
150155
self.end_headers()
151-
self.wfile.write(b'{"ok": true}')
156+
self.wfile.write(payload)
152157

153158
def _record(
154159
self,

tests/test_webhooks_receiver.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def test_receiver_records_verified_delivery() -> None:
3232
with WebhookReceiver(secret=SECRET).run() as rx:
3333
resp = _post_signed(rx.url, body, SECRET)
3434
assert resp.status_code == 200
35+
assert resp.headers["content-type"] == "application/json"
36+
assert resp.json() == {"ok": True}
3537
assert rx.deliveries[0].verified is True
3638
assert rx.deliveries[0].body_bytes == body
3739
assert rx.deliveries[0].body_json == PAYLOAD
@@ -47,6 +49,8 @@ def test_receiver_rejects_bad_signature_with_401() -> None:
4749
timeout=5.0,
4850
)
4951
assert resp.status_code == 401
52+
assert resp.headers["content-type"] == "application/json"
53+
assert resp.json() == {"ok": False, "error": "invalid_signature"}
5054
# The bad delivery is still recorded, marked unverified, so devs
5155
# can debug what arrived.
5256
assert len(rx.deliveries) == 1
@@ -73,6 +77,8 @@ def test_receiver_404s_on_unknown_path() -> None:
7377
wrong = rx.url.replace("/tango/webhooks", "/elsewhere")
7478
resp = httpx.post(wrong, content=b"{}", timeout=5.0)
7579
assert resp.status_code == 404
80+
assert resp.headers["content-type"] == "application/json"
81+
assert resp.json() == {"ok": False, "error": "not_found"}
7682

7783

7884
def test_receiver_invokes_on_delivery_callback() -> None:

0 commit comments

Comments
 (0)