Python http.client: Low-Level HTTP

Published on: August 20, 2026
Reading time: 4 minutes
Server rack representing low-level HTTP connections with Python http.client

The http.client module implements the client side of HTTP and HTTPS at a lower level than urllib.request. It exposes connections, requests, and responses directly, giving precise control over paths, headers, bodies, streaming, connection reuse, proxy tunneling, and TLS contexts.

This control is useful for learning HTTP, testing servers, building specialized clients, and diagnosing integrations. Higher-level libraries are usually more productive for normal APIs. This guide covers the correct HTTPSConnection lifecycle, bounded reads, JSON and file uploads, persistent connections, errors, proxies, and security.

When to use http.client

Use it when you need explicit HTTP/1.1 behavior, custom protocol handling, or no external dependency. For complete URLs, redirects, cookies, and authentication, the urllib.request guide describes a higher standard-library layer. Large applications often benefit from a maintained client with pooling and richer timeout controls.

A first HTTPS connection

import http.client

connection = http.client.HTTPSConnection(
    "www.python.org",
    timeout=10,
)

try:
    connection.request(
        "GET",
        "/",
        headers={
            "Host": "www.python.org",
            "Accept": "text/html",
            "User-Agent": "MyTool/1.0",
        },
    )
    response = connection.getresponse()
    print(response.status, response.reason)
    body = response.read(200_000)
finally:
    connection.close()

The constructor receives a host and optional port, not a complete URL. The request target is normally an absolute path such as /docs?page=1. Always set a timeout.

HTTPS and SSLContext

HTTPSConnection verifies certificates and hostnames by default. Load a private CA with a secure context when necessary.

import ssl

context = ssl.create_default_context(cafile="company-ca.pem")
connection = http.client.HTTPSConnection(
    "internal-api.example",
    timeout=10,
    context=context,
)

Do not pass an unverified context to work around failures. Repair the trust chain. The Python ssl guide explains secure TLS settings.

The request and getresponse lifecycle

Send a request, obtain the response, and fully read or close that response before sending another request on the same connection.

connection.request("GET", "/first")
first = connection.getresponse()
first_data = first.read()

connection.request("GET", "/second")
second = connection.getresponse()
second_data = second.read()

Unread bytes prevent the next response from being framed correctly. Large bodies should be consumed in chunks until EOF.

Bounded response reads

MAX_BYTES = 5 * 1024 * 1024

def read_limited(response, maximum=MAX_BYTES) -> bytes:
    chunks = []
    total = 0

    while chunk := response.read(64 * 1024):
        total += len(chunk)
        if total > maximum:
            response.close()
            raise ValueError("Response exceeded the limit")
        chunks.append(chunk)

    return b"".join(chunks)

Content-Length is useful but not sufficient. Count the bytes actually received. Stream downloads to a temporary file rather than retaining every chunk.

Reading headers

response = connection.getresponse()
content_type = response.getheader("Content-Type")
content_length = response.getheader("Content-Length")
all_headers = response.getheaders()

getheader() joins repeated values with commas. That is not correct for every field, especially Set-Cookie. Use getheaders() when duplicate fields must remain separate.

Sending JSON

import json

payload = json.dumps({"name": "Example"}).encode("utf-8")
headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Content-Length": str(len(payload)),
}

connection.request("POST", "/items", body=payload, headers=headers)
response = connection.getresponse()
raw = read_limited(response, 500_000)

For a bytes body, the library can calculate Content-Length automatically. An explicit value must match the exact number of bytes.

Uploading a file or iterable

When a body is a file or general iterable and no length is provided, the library normally uses chunked transfer encoding.

with open("file.bin", "rb") as file:
    connection.request(
        "PUT",
        "/upload",
        body=file,
        headers={"Content-Type": "application/octet-stream"},
    )
    response = connection.getresponse()
    response.read()

Some legacy servers do not accept chunked request bodies. Provide a correct length when required. A file body may not be safely replayable after authentication or network failure unless repositioned.

Streaming a download

from pathlib import Path

connection.request("GET", "/download")
response = connection.getresponse()

if response.status != 200:
    response.read(20_000)
    raise RuntimeError(f"HTTP {response.status}")

with Path("download.tmp").open("wb") as output:
    total = 0
    while chunk := response.read(64 * 1024):
        total += len(chunk)
        if total > 100 * 1024 * 1024:
            response.close()
            raise ValueError("File is too large")
        output.write(chunk)

Verify a digest before moving the file into place. The Python hashlib guide covers SHA-256 verification.

Handling HTTP status

http.client does not raise an exception for 404 or 500. Inspect response.status yourself.

if 200 <= response.status < 300:
    data = read_limited(response)
elif response.status == 404:
    response.read(20_000)
    raise LookupError("Resource not found")
else:
    response.read(20_000)
    raise RuntimeError(f"Server returned {response.status}")

Redirects, 429, Retry-After, and authentication challenges are also application responsibilities.

Persistent connections

HTTP/1.1 allows reuse, reducing TCP and TLS handshakes. Reuse only after consuming each response. Do not use the same connection concurrently from multiple threads without strict coordination.

If RemoteDisconnected or another connection failure occurs before an idempotent operation, close and reconnect. Do not blindly replay POST because the server may have processed it.

HEAD requests

connection.request("HEAD", "/file.zip")
response = connection.getresponse()
print(response.status, response.getheader("Content-Length"))
response.read()

HEAD has no response body, but complete the response lifecycle. Reported metadata does not replace actual limits during a later GET.

CONNECT proxy tunnels

proxy = http.client.HTTPSConnection("proxy.example", 8443, timeout=10)
proxy.set_tunnel(
    "www.python.org",
    443,
    headers={"Host": "www.python.org:443"},
)
proxy.request("GET", "/")
response = proxy.getresponse()

Protect proxy credentials and validate the destination certificate. Since Python 3.12, CONNECT uses HTTP/1.1 and get_proxy_response_headers() exposes the proxy response headers.

Step-by-step request APIs

putrequest(), putheader(), endheaders(), and send() expose lower protocol stages. Use them only when request() is insufficient; manual header and chunk framing mistakes are easy to introduce.

Exceptions and uncertain state

try:
    connection.request("GET", "/")
    response = connection.getresponse()
    data = read_limited(response)
except (TimeoutError, OSError, http.client.HTTPException) as error:
    connection.close()
    raise RuntimeError("HTTP failure") from error

Important exceptions include IncompleteRead, BadStatusLine, LineTooLong, ResponseNotReady, and RemoteDisconnected. Close the connection whenever framing or state is uncertain.

Debug output

set_debuglevel(1) prints protocol details to stdout. Use it only in controlled development because authorization and other sensitive headers may be displayed.

SSRF protection

If host and port come from an external user, validate DNS and resolved IPs before connecting. Block loopback, private, link-local, and cloud metadata ranges. Direct host connections remain vulnerable to SSRF and DNS rebinding.

Choosing a higher-level client

Use urllib.request or a third-party HTTP client when redirects, cookies, URL parsing, authentication, and connection pools are needed. http.client is best for precise control, education, and narrowly scoped infrastructure.

The REST API guide covers a complete integration workflow.

Common mistakes

Common errors are missing timeouts, passing a full URL as a normal path, failing to consume a response, sharing one connection unsafely, unlimited body reads, disabling TLS validation, replaying POST after failure, and enabling debug output around credentials.

Best practices

Use HTTPS with a validated context, explicit timeout, bounded bodies, streaming, and guaranteed cleanup. Consume or close each response. Reuse only healthy connections, distinguish idempotent methods, and restrict external hosts when possible.

Conclusion

http.client provides direct access to Python’s HTTP/1.1 client. It offers control over connections, requests, responses, streaming, and proxies, while leaving redirects, status policy, retries, and limits to your code. Use it when that control justifies the added responsibility.

Read the official http.client documentation and RFC 9112 for HTTP/1.1.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    HTML code on a screen representing responsible crawling with Python robotparser
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python robotparser: Read robots.txt

    Learn Python urllib.robotparser to respect robots.txt, crawl-delay, request-rate, sitemaps, caching, and responsible crawling limits.

    Ler mais

    Tempo de leitura: 4 minutos
    20/08/2026
    Keyboard tiles spelling HTTP representing Python urllib.request requests
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python urllib.request: Native HTTP

    Learn Python urllib.request for GET, POST, JSON, downloads, TLS, redirects, proxies, size limits, and robust HTTP error handling.

    Ler mais

    Tempo de leitura: 4 minutos
    19/08/2026
    Ethernet cables connected to a network switch representing Python socketserver services
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python socketserver: Build Servers

    Learn Python socketserver to build TCP and UDP servers with handlers, concurrency, framing, limits, timeouts, and graceful shutdown.

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026
    Illuminated server room representing secure TLS connections with Python ssl
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ssl: Secure TLS Connections

    Learn Python ssl to build TLS clients and servers, validate certificates and hostnames, configure CA trust, minimum versions, and mTLS.

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026
    Account verification screen representing message authentication with Python HMAC
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python HMAC: Authenticate Messages

    Learn Python HMAC to sign and verify webhooks, files, and service messages with SHA-256, safe keys, canonical data, and replay

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026
    Fingerprint scanner representing digest verification with Python hashlib
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python hashlib: Secure Hashes

    Learn Python hashlib for SHA-256, file verification, BLAKE2, key derivation, password hashing decisions, and safer digest handling.

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026