Skip to content

Building a simple HTTP server and testing with curl

The Wireshark lab captured a request against someone else's server — example.com, a machine you have no control over and can't add a broken route or a missing header to on purpose. This lab flips that: you run the server, so you control exactly what it returns, and you use curl to probe it the way you'd probe any HTTP service you're debugging.

Goal and end result

By the end of this lab, you'll have a working HTTP server on your own machine, and you'll have used curl to request a page that exists, a page that doesn't, and the same request with only headers returned — three requests that expose three different, genuinely useful pieces of curl's behavior.

Requirements

  • Python 3, which every recent Linux distribution ships by default — this lab uses its built-in http.server module, which needs no installation and no code of your own to serve static files.
  • curl.

Safety note

Python's http.server module's own documentation is explicit that it "is not recommended for production" — it has no access controls, no rate limiting, and no protection against the kinds of abuse a real web server handles as a matter of course. That's fine for this lab, which runs it bound to 127.0.0.1 only, reachable from nowhere but this same machine — never run it bound to 0.0.0.0 or a public interface, for the exact reason the sockets article already warned about binding to all interfaces.

Step 1: create something for the server to serve

mkdir -p ~/http-lab && cd ~/http-lab
echo '<h1>Hello from the lab</h1>' > index.html

Step 2: start the server, bound to loopback only

python3 -m http.server 8000 --bind 127.0.0.1
Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...

--bind 127.0.0.1 is doing the safety work here — without it, http.server defaults to listening on all interfaces, exactly the 0.0.0.0 behavior this course has already flagged more than once as the thing that turns a harmless local test tool into something reachable from a public network the moment it runs on a machine that has one. Leave this running in its own terminal; every following step uses a second terminal.

Step 3: request the page that exists

curl http://127.0.0.1:8000/
<h1>Hello from the lab</h1>

Check the server's own terminal, and it will have logged the request:

127.0.0.1 - - [27/07/2026 14:04:29] "GET / HTTP/1.1" 200 -

That log line is the server's own confirmation that it received exactly the request curl sent and answered with a 200. For a byte-level look at the request and response themselves rather than just this summary, curl -v shows the full exchange — the curl -v article already covered how to read that output line by line, and running it here against a server you built yourself is worth doing as a direct follow-up to this step.

Step 4: request a page that doesn't exist

curl -v http://127.0.0.1:8000/missing
< HTTP/1.0 404 File not found

Two things worth noticing here, both real and both easy to miss. First, the status is 404, Not Found, for exactly the reason that status code exists: the server understood the request perfectly, there's simply nothing at that path. Second — and this is specific to http.server, not a general HTTP fact — the protocol version in the response is HTTP/1.0, not 1.1. Python's built-in server implements the older, simpler version of HTTP by default; every "modern" HTTP behavior the HTTP evolution module covers — persistent connections without an explicit header, pipelining, multiplexing — either doesn't apply here or needs to be requested explicitly, which is exactly why this tool is fine for a quick local test and never appropriate for anything real-world facing.

Step 5: request only the headers

curl -I http://127.0.0.1:8000/
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.3
Date: Mon, 27 Jul 2026 09:04:50 GMT
Content-type: text/html
Content-Length: 28
Last-Modified: Mon, 27 Jul 2026 09:04:28 GMT

-I sends a HEAD request instead of GET — a request that asks for the same headers a GET would return, without the body. This is genuinely useful outside a lab: checking whether a URL exists, what content type it reports, or how large the body is, without downloading the body itself, which matters when the concern is a multi-gigabyte file and all you actually want to know is whether it's there at all.

Practical scenario: a Content-Length that doesn't match

Add a second file and request it, then deliberately edit it while the server is still running:

echo 'Original content here.' > page.html
curl -sI http://127.0.0.1:8000/page.html | grep -i content-length
Content-Length: 24

Now overwrite the file with something longer, without restarting the server:

echo 'This is now a considerably longer piece of content than before.' > page.html
curl -sI http://127.0.0.1:8000/page.html | grep -i content-length
Content-Length: 67

The header changed to match the new file size on the very next request — http.server computes Content-Length fresh for every request rather than caching it, which is a reasonable default for a file that might genuinely change, but is exactly the kind of behavior that becomes a real production concern at scale: a server recalculating a response's size on every single request, rather than caching what it can, is doing real work per request that a busier service would want to avoid. This is a small, concrete preview of the caching considerations the CDN articles go into in far more depth.

Troubleshooting

  • curl: (7) Failed to connect to 127.0.0.1 port 8000. The server isn't running, or is bound to a different port than the one curl is requesting — check the server's own terminal for the exact port it printed on startup.
  • The server's terminal shows nothing after a request. Confirm the request actually reached 127.0.0.1:8000 and not a different address — a typo pointing at a LAN address instead of loopback would simply time out rather than reach this server at all.
  • curl -I hangs instead of returning immediately. This can happen if a firewall on the loopback interface is unusually configured, though it's rare — confirm with a plain curl -v first to see exactly where the exchange stalls.

Cleanup

Stop the server with Ctrl-C in its terminal, then remove the lab directory:

rm -rf ~/http-lab

Evaluation criterion

You've completed this lab correctly if you can show, from your own terminal output, all three: a successful 200 response with the expected body, a 404 for a path that doesn't exist, and a HEAD response showing headers only, no body — and explain in one sentence why Content-Length changed between the two requests in the practical scenario above.

Every request in this lab traveled over plain HTTP, readable in full by anyone positioned on the path between client and server — exactly the gap TLS exists to close. The next lab observes that exact handshake happening, live, against a real HTTPS site.

Sources