Tiny JSON echo server for testing HTTP clients
A standard-library Python server that echoes back the method, path, query, headers, and body of every request, and can return any status code on demand.
When a request does not do what you expect, the fastest check is to send it to a server that shows you exactly what arrived. This script is that server. It needs only Python 3.7 or newer and listens on 127.0.0.1, so nothing outside your machine can reach it.
The script
#!/usr/bin/env python3
"""Tiny JSON echo server for testing HTTP clients. Standard library only."""
import json
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit, parse_qs
MAX_BODY = 1_000_000 # bytes; larger bodies get 413
class EchoHandler(BaseHTTPRequestHandler):
def _send_json(self, status, payload):
body = json.dumps(payload, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def _handle(self):
url = urlsplit(self.path)
# /status/404 answers with that status code
parts = url.path.strip("/").split("/")
if len(parts) == 2 and parts[0] == "status" and parts[1].isdigit():
code = int(parts[1])
if 200 <= code <= 599:
self._send_json(code, {"status": code})
return
length = int(self.headers.get("Content-Length") or 0)
if length > MAX_BODY:
self.close_connection = True
self._send_json(413, {"error": "body too large", "limit": MAX_BODY})
return
raw = self.rfile.read(length) if length else b""
body = raw.decode("utf-8", errors="replace")
if "json" in (self.headers.get("Content-Type") or ""):
try:
body = json.loads(raw or b"null")
except json.JSONDecodeError as exc:
self._send_json(400, {"error": "invalid JSON", "detail": str(exc)})
return
self._send_json(200, {
"method": self.command,
"path": url.path,
"query": parse_qs(url.query),
"headers": dict(self.headers),
"body": body,
})
do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = do_HEAD = _handle
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
server = ThreadingHTTPServer(("127.0.0.1", port), EchoHandler)
print(f"Echo server on http://127.0.0.1:{port} (Ctrl+C to stop)")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
Running it
python3 echo_server.py # listens on port 8765
python3 echo_server.py 9000 # or pick a port
In a second terminal, send it something:
curl -s --json '{"hello": "world"}' 'http://127.0.0.1:8765/items?debug=1'
curl -s -i http://127.0.0.1:8765/status/503
The first command returns a JSON document listing the method, path, parsed query string, every header curl sent, and the parsed body. The second returns a 503 status so you can check how your client handles errors. Stop the server with Ctrl+C.
What it is good for
- Checking that your client really sends the
Authorization,Content-Type, or custom headers you think it does. - Seeing whether a body is JSON, form data, or plain text after a library has encoded it.
- Testing retry and error handling:
/status/429,/status/500, and friends return those codes. - Reproducing a
413by sending more than one megabyte, or a400by sending invalid JSON with a JSON content type.
How it works
BaseHTTPRequestHandler parses the request line and headers for you and calls a method named after the HTTP verb, such as do_GET. The script points all the verbs at one function. The body is read from rfile using the Content-Length header, which is also how the size limit is enforced before reading. ThreadingHTTPServer handles each connection in its own thread, so one slow client does not block others.
This is a debugging tool, not a production server. It has no TLS, does not support chunked request bodies, and prints request headers back, which may include tokens. Keep it bound to localhost.
See also
- 0003 pathlib cheat sheet: files and folders in Python pathlib.Path is the modern way to build paths, read and write files, and list or search folders in Python. This page lists… Beginner · Reference · Any system Article Files & automation 15 Oct 2026
- 0004 Logging instead of print The logging module gives every message a level, a timestamp and a destination you can change without editing code. It is the… Intermediate · Concept · Any system Article Files & automation 15 Oct 2026
- 0009 CSV to JSON converter A small command-line tool that turns a CSV file into a JSON array of objects, with optional number detection, custom delimiters and… Beginner · How-to · Any system Script CSV & JSON · Files & automation 15 Oct 2026
- 0010 Find duplicate files by content Scans folders and lists files with identical content, using file size first and SHA-256 hashes second. It is read-only and only reports… Intermediate · How-to · Any system Script Files & automation 15 Oct 2026
- 0019 fetch with timeout, retries and proper error handling A reusable fetchJson helper for Node 20+ and browsers that adds a timeout, retries with backoff for temporary failures, and clear errors… Intermediate · How-to · Any system Script Async code · Making requests 15 Oct 2026
- 0074 pandas basics: read a CSV, filter rows and summarize with groupby Load a CSV into a pandas DataFrame, inspect it, filter rows with conditions, and compute totals per group, with the SQL equivalent… Intermediate · How-to · Any system Article Data analysis · Files & automation 15 Oct 2026
Repos in the wild
- Requests: HTTP for humans psf/requests Requests is the classic Python library for calling web APIs and downloading pages, with a simple API for GET and POST requests, headers, JSON and sessions. GitHub
- Axios axios/axios A promise-based HTTP client for browsers and Node.js with interceptors, automatic JSON handling, request cancellation and a consistent API in both environments. GitHub
- curl and libcurl (curl/curl) curl/curl The source of curl, the command-line tool for transferring data with URLs, and libcurl, the C library behind it that many other programs use for HTTP. GitHub
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- commitpedia-admin +4,615 chars