I recently built a zero-dependency HTTP client in Python from scratch. No requests, no httpx, not even urllib. Just raw sockets.
Normally, I’d just pip install requests and move on. It’s so standard that we forget what it actually does. Replacing it meant going back to basics: opening raw TCP connections, manually encoding the HTTP request line, and parsing raw byte streams by hand.
Here is what the code actually looks like when we strip away the abstraction, and what I learned in the process.
#. The Hidden Stdlib Gem: Trusting TLS-If we do networking in Python, we've probably seen certifi installed to handle root certificates. I assumed a third-party CA bundle was mandatory. It turns out, it isn't. The ssl module has a hidden gem:** ssl.create_default_context()**.
# 1. Open the raw TCP connection
sock = socket.create_connection((host, port), timeout=15)
# 2. Wrap it in TLS using the OS native trust store
ctx = ssl.create_default_context()
sock = ctx.wrap_socket(sock, server_hostname=host)
The Gotcha: The Python docs make wrapping a socket seem easy. What they don't emphasize is that if we just instantiate a base ssl.SSLContext(), it loads absolutely zero trusted certificates by default. Every single HTTPS request will silently fail with an unknown-CA error. create_default_context() automatically loads our operating system's native trust store and handles hostname verification right out of the box.
#. Building the Request is Just Strings- When we call requests.get(url), it hides the fact that HTTP/1.1 is just a formatted text document sent over a wire. Building it by hand removes the magic. It's just a list of strings joined by carriage returns.
lines = [
f"{method} {path} HTTP/1.1",
f"Host: {host}",
"Connection: close",
"Accept-Encoding: identity", # Disable gzip so we read raw bytes
"User-Agent: httpc/1.0 (zero-dep)",
"" # Blank line terminates headers
]
# The protocol is just ascii strings joined by carriage returns
raw_headers = "\r\n".join(lines).encode("latin-1")
sock.sendall(raw_headers + body)
Seeing this run successfully against a real API makes us realize how simple the web fundamentally is.
#. The Hard Part: Chunked Transfer-Encoding This is the part that turned out much harder than the RFC docs made it look. requests silently pieces chunked bodies together for us. When a server sends Transfer-Encoding: chunked, we don't get a nice, clean Content-Length. We get chunks of data prefixed by their size in hexadecimal.
Doing this manually means writing a loop to parse hex-strings from a raw byte buffer just to figure out how many bytes to read next before the connection closes.
# A simplified look at manually parsing chunked encoding
while pos < len(buf):
# Find the next carriage return
crlf = buf.find(b"\r\n", pos)
# Extract the hex string and parse it into an integer
size_str = buf[pos:crlf].split(b";")[0].strip()
chunk_size = int(size_str, 16)
if chunk_size == 0:
break # The server is done sending data
# Now read exactly `chunk_size` bytes from the socket...
It’s tedious. We have to handle buffer boundaries, strip chunk extensions, and manage state manually. We quickly appreciate why we use libraries for this.
#. The Honest Reality: We didn't fully replace requests to be clear, this project isn't a 1:1 replacement for requests or httpx. We didn't implement connection pooling, we didn't add HTTP/2 support, and we explicitly disabled gzip decompression.
But replacing a library completely wasn't the point. The goal was to build a thin enough slice of the protocol to prove that we can do it, and to understand what the library is actually doing for us under the hood.
The Real Takeaway Abstractions are great for shipping, but terrible for understanding.
When we just pass allow_redirects=True into a library, HTTP routing remains magic. When we have to manually parse a 302 Location header, check the RFC rules, and recursively write the code to downgrade a POST to a GET, the magic disappears and becomes engineering.
If we ever feel stuck in the cycle of just gluing APIs together, rewriting a core tool we use every day using nothing but standard tools is a great exercise. We obviously won't ship it to production, but the mental model we walk away with is permanent.
Check out the code here: https://github.com/PranjaldevX/httpc
Top comments (0)