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

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026