Skip to content
Commitpedia
Log in Join & edit

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.

Edit history of Tiny JSON echo server for testing HTTP clients: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
python
Licence
CC0-1.0

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 413 by sending more than one megabyte, or a 400 by 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.

  1. commitpedia-admin +4,615 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?