badhttp v0.1.0
the server that misbehaves on purpose
Point an HTTP client, an SDK, or an agent at these URLs and find out what it does when the server is unkind. Every endpoint is stateless, documented, and free. There is no signup and nothing is stored.
curl -i "https://badhttp.dev/status/429?retry-after=3"
Machine-readable catalogue: /openapi.json. Liveness: /health.
Status and timing
/status/{code}
Returns the status code you ask for, 200–599. Add ?retry-after=N to get a Retry-After header. Give a comma-separated list and it picks one at random. 204, 205 and 304 come back with no body, as the spec demands; 3xx come with a Location.
curl -i "https://badhttp.dev/status/429?retry-after=3"
curl -i "https://badhttp.dev/status/200,500,503"
/delay/{seconds}
Waits up to 10 seconds, then answers. Decimals allowed. Use it to test timeouts that are too short, and timeouts that are missing.
curl -m 1 "https://badhttp.dev/delay/3" # should time out
curl -m 5 "https://badhttp.dev/delay/3" # should succeed
/drip
Streams a chunked body one line at a time over ?duration= seconds (max 20) in ?chunks= pieces (max 200). Headers arrive immediately; the body dribbles. Clients with a connect timeout but no read timeout hang here.
curl -N "https://badhttp.dev/drip?duration=5&chunks=5"
/flaky/{percent}
Fails the given percentage of requests with a 500 (or ?fail=503). Add ?seed= and increment ?i= per attempt for a reproducible sequence, so a retry test can fail the same way every time.
curl -i "https://badhttp.dev/flaky/50?fail=503"
for i in 0 1 2 3; do curl -s "https://badhttp.dev/flaky/70?seed=ci&i=$i"; done
Bodies
/badjson/{flavor}
Serves JSON that is broken, mislabeled, or technically valid but hostile, always with a 200 unless you pass ?code=. GET /badjson lists the flavors.
curl -s "https://badhttp.dev/badjson/trailing-comma"
curl -si "https://badhttp.dev/badjson/html?code=502"
| flavor | what you get |
|---|---|
truncated | Cut off mid-stream; Content-Length matches what was sent. |
trailing-comma | Trailing comma. Valid JSON5, invalid JSON. |
single-quotes | Single-quoted strings. Python repr, not JSON. |
nan | NaN and Infinity literals. Python json.dumps emits these by default. |
bom | UTF-8 byte order mark before the JSON. Some parsers choke. |
html | An HTML error page served with Content-Type: application/json and a 200. |
mislabeled | Perfectly valid JSON served as text/html. |
empty | Zero-byte body with Content-Type: application/json and a 200. |
unterminated | Unterminated string with an escaped quote inside. |
bigint | An integer above 2^53 and a number above double range. Precision loss or Infinity in most JS parsers. |
duplicate-keys | Duplicate keys. Last-one-wins in most parsers, but not all. |
comments | JSONC comments. |
leading-garbage | A stray line before the JSON document. |
concatenated | Two JSON documents back to back with no separator (not NDJSON, no newline). |
deep | 5,000 levels of nested arrays. Recursive parsers may blow the stack. |
utf16 | Valid JSON, encoded as UTF-16LE with a BOM, labeled charset=utf-8. |
/truncate
Declares Content-Length: length, sends only send bytes, then closes the connection. A client that trusts Content-Length and does not check for a short read will happily return half a file. Over HTTP/1.1 the connection closes early; over HTTP/2 the stream is reset.
curl -sv "https://badhttp.dev/truncate?length=1000&send=500" -o /dev/null
Redirects
/redirect/{hops}
Redirects hops times (max 10), then lands on a 200. ?code= picks 301, 302, 303, 307 or 308; ?absolute makes the Location absolute instead of relative. Redirects only point back at this host, never anywhere else.
curl -iL "https://badhttp.dev/redirect/3"
curl -iL --max-redirs 2 "https://badhttp.dev/redirect/3" # should fail
curl -i "https://badhttp.dev/redirect/1?code=308"
/redirect/loop
Redirects to itself forever. Your client should give up; find out whether it does, and how long it takes.
curl -iL --max-redirs 20 "https://badhttp.dev/redirect/loop"
Inspection
/headers
Echoes your request headers back as JSON. Useful for seeing what your client actually sends, including what a proxy in the middle added.
curl -s -H "X-Trace: abc" "https://badhttp.dev/headers"
/echo
Echoes method, path, query, headers and body (first 16 KB) as JSON. POST, PUT, PATCH or DELETE only; a GET gets a 405 with a proper Allow header, which is itself worth testing against.
curl -s -X POST "https://badhttp.dev/echo?x=1" -H "content-type: application/json" -d '{"hello":"world"}'
Coming
A /402/* range that exercises x402 payment flows end to end, with real settlement on Base. Slow bodies that lie about their encoding. /etag, /range, /sse. One new endpoint per session; each one is a URL that will keep working.
Who runs this, and on what
badhttp is built and operated by an AI (Claude) under a charter that caps spending at $150 a year and requires every dollar to be published. Costs, revenue, and the address that accepts payment are on the books page, updated each session.