Type a raw HTTP request into a TCP socket, read back a real 200 OK, then watch one wrong character kill it. No browser, no library.
Prerequisites
- A Linux terminal (any distro; a VM is fine)
- bash or zsh
- Internet access
Steps
1. Check for netcat and identify the variant.
which nc && nc -h 2>&1 | head -1
Expect: a path such as /usr/bin/nc, then a line naming the build — OpenBSD netcat or Ncat 7.xx. Write down which one; steps 11 and 12 use a different flag on each. Anything else, such as a bare version like [v1.10-47], is netcat-traditional — use the flags exactly as written. If nothing prints, continue to step 2. Otherwise skip to step 3.
2. Install netcat.
sudo apt install netcat-openbsd # Debian, Ubuntu
sudo dnf install nmap-ncat # Fedora, RHEL
On other distros, install your package manager's OpenBSD netcat package. Re-run step 1. Expect: a path and a variant name.
3. Confirm outbound port 80 works.
nc -vz example.com 80
Expect: a success message naming the port. A timeout or a name-resolution error means your network blocks outbound port 80 or cannot resolve the host — nothing below will work until you move to a network that permits both. If -z is rejected, see Troubleshooting.
4. Store the request.
REQ='GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n'
Use single quotes. They stop the shell from expanding anything inside the string, which matters once you add header values containing $ or a backtick.
5. Send the request.
printf '%b' "$REQ" | nc example.com 80 | tee response.txt
%b makes printf interpret the \r\n escapes instead of printing them as text; tee writes to the file and the screen at once.
Expect: HTTP/1.1 200 OK, a block of headers, a blank line, then HTML. The body scrolls past — read it from the file in the steps below. Re-running overwrites the file.
6. Read the status line.
head -1 response.txt
Expect: three parts in order — protocol version, status code, reason phrase.
7. Read the header block.
sed -n '1,/^\r$/p' response.txt
Look for Content-Length or Transfer-Encoding, and for Allow. Any of them may be absent.
8. Open the whole response.
less response.txt
Every header line ends in ^M, as do the chunk-size lines further down. That is the carriage return you sent, not corruption. Press q to exit.
9. Find the first line after the headers.
sed -n '/^\r$/{n;p;q;}' response.txt
The {n;p;q;} advances one line past the blank line, prints it, and stops. Applies only if step 7 showed Transfer-Encoding: chunked. Expect: a short hex number such as 22f.
10. Read the end of the response.
tail -2 response.txt
Applies only to a chunked response. Expect: a lone 0, then a line that looks blank. It isn't — it holds the carriage return from the terminating chunk's own CRLF, the same ^M you saw in step 8.
11. Break the terminator on purpose.
BAD='GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close/r/n/r/n'
printf '%b' "$BAD" | nc -w 5 example.com 80
On nmap-ncat, use -i 5 instead of -w 5 — its -w times the connect only and will not close an idle socket.
Expect: nothing — no status line, no error, no body. The flag closes the socket after five idle seconds. Press Ctrl+C to exit early.
Silence is what you see inside that window. Drop the timeout and wait, and some servers eventually answer 408 Request Timeout — the same finding stated differently. The server never saw a complete request.
12. Repeat with more headers.
BAD='GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: curl\r\nAccept: */*\r\nConnection: close/r/n/r/n'
printf '%b' "$BAD" | nc -w 5 example.com 80
Same flag swap as step 11 on nmap-ncat. Expect: identical silence. Four headers behave exactly like two.
Verification check
printf '%b' "$REQ" | nc example.com 80 | head -1
Exactly one line prints: HTTP/1.1 200 OK. Anything else means the request is malformed. If nothing prints, re-run step 3 — a success there means the port is open and the fault is in your request.
Troubleshooting
No output — the command sits, then returns to the prompt.
Copy the command rather than retyping it. The terminator needs backslashes: \r\n\r\n. printf gives forward slashes no special meaning, so /r/n/r/n lands inside the last header value as text and the terminator never gets sent.
Nothing happens and echo "$REQ" prints an empty line.
Re-run step 4. Shell variables do not survive a new terminal window or tab.
Step 11 or 12 hangs and never returns.
You are on nmap-ncat, where -w covers the connect only. Use -i 5. Ctrl+C to break out of the current run.
Your netcat rejects -z as an unknown option.
Not all builds include it. Use nc -vw 5 example.com 80 < /dev/null instead. -w is correct on every variant here, since this test only needs the connect to succeed or fail. The -v prints the result — without it you get silence whether the port is open or closed.
You expected 400 Bad Request and got silence.
Treat the hang as the normal symptom of a broken terminator, not a network fault. With no terminator, the server has nothing to reject — it is still listening.
Header values are not parsed as expected.
Put one space after every colon when you add your own headers. Host:example.com is widely tolerated but not guaranteed.
Closing
One wrong character — / for \ — is the difference between a page and total silence. HTTP is plain text over a socket, and it has no tolerance for text that is almost right.
Drafted and revised with Claude; every command tested by me.
Top comments (0)