HTTP by Hand
Here is the moment netcat exists for. Serve the lab's specimen page with Python's built-in web server:
$ cd examples
$ python3 -m http.server 45003 --bind 127.0.0.1
(its own terminal — it will log every request there, which is half the fun). Now, from another terminal, fetch the page the way a browser does, except that you are the browser:
$ printf "GET / HTTP/1.0\r\n\r\n" | nc -N -w 3 127.0.0.1 45003
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.3
Date: Tue, 20 Oct 2026 19:21:33 GMT
Content-type: text/html
Content-Length: 286
Last-Modified: Tue, 20 Oct 2026 19:20:55 GMT
<!DOCTYPE html>
<html>
<head><title>Lab Web - Specimen Page</title></head>
<body>
<h1>Specimen page</h1>
<p>This page exists so the netcat lab has something real
to fetch by hand. If you are reading this in a terminal,
you typed an HTTP request yourself. Well done.</p>
</body>
</html>
Your entire contribution to that
exchange was one line — GET / HTTP/1.0
— followed by a blank line (the
\r\n\r\n is two carriage-return +
line-feed pairs: the end of the request
line, then the empty line that says
"that's all"). Everything else in the
capture is the server:
- The status line —
HTTP/1.0 200 OK: the protocol's version, the code, and the code's name in words. 200 is the famous one; you meet another below. - The headers — one
Name: valueper line: what the server is (Server:), when it answered (Date:), what it's sending (Content-type:), and — the one netcat cares about most — exactly how many bytes of body follow (Content-Length: 286). - A blank line, the mirror of yours: headers over.
- The body — 286 bytes of it, the specimen page itself.
That's an HTTP transaction with the
envelope removed. When the curl guide
shows you -v output with its > and
< lines, this is the conversation
it's annotating; you have now had it
without the annotation.
Asking for something that isn't there
$ printf "GET /no-such-page HTTP/1.0\r\n\r\n" | nc -N -w 3 127.0.0.1 45003
HTTP/1.0 404 File not found
Server: SimpleHTTP/0.6 Python/3.12.3
Date: Tue, 20 Oct 2026 19:21:33 GMT
Connection: close
Content-Type: text/html;charset=utf-8
Content-Length: 335
...
Same shape, different story: the status
line carries the famous 404, and the
body is an apology in HTML. (The capture
is trimmed at the body here — the full
error page is in the lab transcripts.)
The lesson is worth the detour: an HTTP
"error" is not a failed conversation.
The socket worked perfectly; the
protocol said no. Netcat's exit code
agrees — this exchange also ends 0,
because netcat's job is the socket, and
the socket succeeded. Tools that care
about the 404 (curl's --fail, your
browser's sad face) care on top of the
socket, in the protocol layer netcat
deliberately doesn't have.
Why bother, when curl exists?
Three mortal reasons:
- The server is lying and you want the raw tape. Redirects, browser content negotiation, and helpful libraries all edit the conversation. Netcat shows the bytes that actually crossed.
- There's no curl there. A minimal
container, a broken appliance, a
rescue shell: if it has
nc, you can still ask the web service what it thinks. - The service isn't HTTP. The same
by-hand posture works on any
text-shaped protocol: greet a mail
server, type
EHLO me, read the reply. The protocols differ; the handset doesn't.
One limit, plainly: HTTPS will not
talk to you this way. A TLS server
expects a cryptographic handshake, not
text — netcat will connect, and then
the two of you will sit in encrypted
silence. For the by-hand TLS
conversation, the tool is openssl
s_client and its own guide in this
series; ncat --ssl (chapter 12) is
the netcat-family answer, but it is not
the OpenBSD nc taught here.
Check: look at the web server's own
terminal after both fetches. It logged
both requests — "GET / HTTP/1.0" 200
and "GET /no-such-page HTTP/1.0" 404 —
the server's side of the same two
conversations, kept whether the client
was you, netcat, or a browser.