Python’s http.client module implements HTTP/1.1 connections directly on top of sockets. import http.client. HTTPConnection: conn = http.client.HTTPConnection("example.com", timeout=10) — plain HTTP. HTTPSConnection: conn = http.client.HTTPSConnection("example.com", context=ssl.create_default_context(), timeout=10) — TLS. request: conn.request("GET", "/path", headers={"Accept": "application/json"}) — send request; body can be str or bytes. getresponse: resp = conn.getresponse() — returns HTTPResponse; resp.status (200), resp.reason (“OK”), resp.version (11). Read body: data = resp.read() — reads all (consumes response); resp.read(n) for streaming. headers: resp.getheader("Content-Type"), resp.getheaders() → list of (name, value) tuples. Reuse: conn.close() / conn.connect() for explicit lifecycle; reuse connection for keep-alive by calling request again after resp.read(). POST: body as bytes; set Content-Type and Content-Length headers. set_debuglevel: conn.set_debuglevel(1) — prints headers to stderr. Redirects not followed automatically — check resp.status in 3xx range manually. http.client.responses maps status codes to reason phrases. Claude Code generates REST API clients, webhook senders, health-check pollers, and HTTP/1.1 proxy implementations.
CLAUDE.md for http.client
## http.client Stack
- Stdlib: import http.client, ssl, json, urllib.parse
- HTTPS: conn = http.client.HTTPSConnection(host, context=ssl.create_default_context(), timeout=10)
- Send: conn.request("GET", "/path", headers={"Accept": "application/json"})
- Recv: resp = conn.getresponse(); body = resp.read()
- JSON: payload = json.loads(body)
- Reuse: call conn.request() again after resp.read() for keep-alive
http.client HTTP Client Pipeline
# app/httputil.py — GET/POST/JSON helpers, retries, streaming, connection pool
from __future__ import annotations
import http.client
import json
import ssl
import time
import urllib.parse
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Any, Generator, Iterator
# ─────────────────────────────────────────────────────────────────────────────
# 1. Response wrapper
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class HttpResponse:
status: int
reason: str
headers: dict[str, str]
body: bytes
@property
def ok(self) -> bool:
return 200 <= self.status < 300
def text(self, encoding: str = "utf-8") -> str:
return self.body.decode(encoding)
def json(self) -> Any:
return json.loads(self.body)
def __str__(self) -> str:
return f"HTTP {self.status} {self.reason} ({len(self.body)} bytes)"
def _build_response(resp: http.client.HTTPResponse) -> HttpResponse:
headers = {k.lower(): v for k, v in resp.getheaders()}
body = resp.read()
return HttpResponse(
status=resp.status,
reason=resp.reason,
headers=headers,
body=body,
)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Connection helpers
# ─────────────────────────────────────────────────────────────────────────────
def _make_conn(
host: str,
port: int | None = None,
https: bool = True,
timeout: float = 10.0,
ssl_context: ssl.SSLContext | None = None,
) -> http.client.HTTPConnection:
if https:
ctx = ssl_context or ssl.create_default_context()
return http.client.HTTPSConnection(host, port=port, context=ctx, timeout=timeout)
return http.client.HTTPConnection(host, port=port, timeout=timeout)
@contextmanager
def http_conn(
host: str,
port: int | None = None,
https: bool = True,
timeout: float = 10.0,
) -> Generator[http.client.HTTPConnection, None, None]:
"""
Context manager that opens and closes an HTTP(S) connection.
Example:
with http_conn("httpbin.org") as conn:
resp = get(conn, "/get")
print(resp.json())
"""
conn = _make_conn(host, port=port, https=https, timeout=timeout)
try:
yield conn
finally:
conn.close()
# ─────────────────────────────────────────────────────────────────────────────
# 3. Core request methods
# ─────────────────────────────────────────────────────────────────────────────
_DEFAULT_HEADERS = {"User-Agent": "python-http.client/stdlib", "Accept": "*/*"}
def request(
conn: http.client.HTTPConnection,
method: str,
path: str,
*,
headers: dict[str, str] | None = None,
body: bytes | str | None = None,
params: dict[str, str] | None = None,
) -> HttpResponse:
"""
Send an HTTP request over an existing connection; return HttpResponse.
Example:
with http_conn("httpbin.org") as conn:
resp = request(conn, "GET", "/status/200")
print(resp.status)
"""
if params:
path = path + "?" + urllib.parse.urlencode(params)
hdrs = dict(_DEFAULT_HEADERS)
if headers:
hdrs.update(headers)
raw_body: bytes | None = None
if body is not None:
raw_body = body.encode() if isinstance(body, str) else body
hdrs.setdefault("Content-Length", str(len(raw_body)))
conn.request(method, path, body=raw_body, headers=hdrs)
return _build_response(conn.getresponse())
def get(conn: http.client.HTTPConnection, path: str, **kwargs) -> HttpResponse:
"""
GET request.
Example:
with http_conn("httpbin.org") as conn:
resp = get(conn, "/get", params={"q": "test"})
"""
return request(conn, "GET", path, **kwargs)
def post(
conn: http.client.HTTPConnection,
path: str,
payload: Any,
content_type: str = "application/json",
**kwargs,
) -> HttpResponse:
"""
POST request with JSON or form body.
Example:
with http_conn("httpbin.org") as conn:
resp = post(conn, "/post", {"key": "value"})
print(resp.json())
"""
if content_type == "application/json":
body = json.dumps(payload).encode()
elif content_type == "application/x-www-form-urlencoded":
body = urllib.parse.urlencode(payload).encode()
else:
body = payload if isinstance(payload, bytes) else str(payload).encode()
headers = kwargs.pop("headers", {})
headers["Content-Type"] = content_type
return request(conn, "POST", path, body=body, headers=headers, **kwargs)
def put(conn: http.client.HTTPConnection, path: str, payload: Any, **kwargs) -> HttpResponse:
"""PUT request with JSON body."""
body = json.dumps(payload).encode()
headers = kwargs.pop("headers", {})
headers["Content-Type"] = "application/json"
return request(conn, "PUT", path, body=body, headers=headers, **kwargs)
def delete(conn: http.client.HTTPConnection, path: str, **kwargs) -> HttpResponse:
"""DELETE request."""
return request(conn, "DELETE", path, **kwargs)
def head(conn: http.client.HTTPConnection, path: str, **kwargs) -> HttpResponse:
"""HEAD request — returns headers only (body will be empty)."""
return request(conn, "HEAD", path, **kwargs)
# ─────────────────────────────────────────────────────────────────────────────
# 4. One-shot convenience functions (open + close per call)
# ─────────────────────────────────────────────────────────────────────────────
def fetch(
url: str,
method: str = "GET",
*,
headers: dict[str, str] | None = None,
body: bytes | str | None = None,
params: dict[str, str] | None = None,
timeout: float = 10.0,
) -> HttpResponse:
"""
One-shot HTTP request by full URL. Opens and closes the connection.
Example:
resp = fetch("https://httpbin.org/get", params={"foo": "bar"})
print(resp.json())
"""
parsed = urllib.parse.urlsplit(url)
https = parsed.scheme == "https"
host = parsed.netloc or parsed.path
path = parsed.path or "/"
if parsed.query:
path += "?" + parsed.query
with http_conn(host, https=https, timeout=timeout) as conn:
return request(conn, method, path, headers=headers, body=body, params=params)
def fetch_json(url: str, **kwargs) -> Any:
"""
Fetch URL and parse JSON body.
Example:
data = fetch_json("https://httpbin.org/json")
"""
resp = fetch(url, **kwargs)
if not resp.ok:
raise RuntimeError(f"HTTP {resp.status}: {resp.reason}")
return resp.json()
# ─────────────────────────────────────────────────────────────────────────────
# 5. Retry and streaming helpers
# ─────────────────────────────────────────────────────────────────────────────
def fetch_with_retry(
url: str,
max_attempts: int = 3,
backoff: float = 1.0,
retry_on: tuple[int, ...] = (429, 500, 502, 503, 504),
**kwargs,
) -> HttpResponse:
"""
Fetch URL with exponential-backoff retry on transient errors.
Example:
resp = fetch_with_retry("https://api.example.com/data", max_attempts=4)
"""
delay = backoff
last: HttpResponse | None = None
for attempt in range(max_attempts):
try:
resp = fetch(url, **kwargs)
if resp.status not in retry_on:
return resp
last = resp
except (OSError, TimeoutError) as exc:
if attempt == max_attempts - 1:
raise
last = None # type: ignore[assignment]
if attempt < max_attempts - 1:
time.sleep(delay)
delay *= 2.0
if last is not None:
return last
raise RuntimeError(f"All {max_attempts} attempts failed")
def stream_response(
conn: http.client.HTTPConnection,
path: str,
chunk_size: int = 8192,
**kwargs,
) -> Iterator[bytes]:
"""
Stream a GET response in chunks (lazy body reading).
Example:
with http_conn("example.com") as conn:
for chunk in stream_response(conn, "/large-file"):
process(chunk)
"""
hdrs = dict(_DEFAULT_HEADERS)
hdrs.update(kwargs.get("headers", {}))
conn.request("GET", path, headers=hdrs)
resp = conn.getresponse()
if not (200 <= resp.status < 300):
resp.read()
raise RuntimeError(f"HTTP {resp.status}: {resp.reason}")
while True:
chunk = resp.read(chunk_size)
if not chunk:
break
yield chunk
@dataclass
class ServiceHealth:
host: str
path: str
status: int
latency_ms: float
ok: bool
def __str__(self) -> str:
state = "UP" if self.ok else "DOWN"
return f"{self.host}{self.path} {state} {self.status} {self.latency_ms:.1f}ms"
def health_check(host: str, path: str = "/", timeout: float = 5.0, https: bool = True) -> ServiceHealth:
"""
HTTP health check: measure latency and check for 2xx status.
Example:
hc = health_check("httpbin.org", "/status/200")
print(hc)
"""
t0 = time.monotonic()
try:
with http_conn(host, https=https, timeout=timeout) as conn:
resp = get(conn, path)
latency = (time.monotonic() - t0) * 1000
return ServiceHealth(host=host, path=path, status=resp.status,
latency_ms=latency, ok=resp.ok)
except Exception:
latency = (time.monotonic() - t0) * 1000
return ServiceHealth(host=host, path=path, status=0, latency_ms=latency, ok=False)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== http.client demo ===")
# ── fetch JSON ────────────────────────────────────────────────────────────
print("\n--- fetch_json ---")
try:
data = fetch_json("https://httpbin.org/json")
print(f" keys: {list(data.keys())[:4]}")
except Exception as e:
print(f" network error: {e} (offline?)")
# ── POST JSON ─────────────────────────────────────────────────────────────
print("\n--- POST ---")
try:
with http_conn("httpbin.org") as conn:
resp = post(conn, "/post", {"message": "hello", "value": 42})
print(f" status={resp.status} ok={resp.ok}")
parsed = resp.json()
print(f" echo json keys: {list(parsed.get('json', {}).keys())}")
except Exception as e:
print(f" network error: {e} (offline?)")
# ── HEAD request ──────────────────────────────────────────────────────────
print("\n--- HEAD ---")
try:
with http_conn("httpbin.org") as conn:
resp = head(conn, "/get")
print(f" status={resp.status} content-type={resp.headers.get('content-type', '?')}")
except Exception as e:
print(f" network error: {e} (offline?)")
# ── health check ──────────────────────────────────────────────────────────
print("\n--- health_check ---")
try:
hc = health_check("httpbin.org", "/status/200")
print(f" {hc}")
except Exception as e:
print(f" network error: {e} (offline?)")
# ── status code lookup ────────────────────────────────────────────────────
print("\n--- http.client.responses ---")
for code in (200, 201, 301, 400, 404, 429, 500, 503):
print(f" {code}: {http.client.responses.get(code, '?')}")
print("\n=== done ===")
For the requests / httpx alternative — requests (PyPI) adds automatic redirect following, session-level cookie jars, connection pooling with Session, and a far simpler API (requests.get(url)) without manually managing connections; httpx also supports HTTP/2 and async; both handle gzip/deflate decompression and multipart form encoding transparently — use requests or httpx for all production HTTP API integration where developer ergonomics matter; use http.client when you need zero external dependencies, are building a library that cannot take on PyPI packages, or require byte-level control over the wire format. For the urllib.request alternative — urllib.request wraps http.client with a handler chain including HTTPRedirectHandler, HTTPCookieProcessor, and proxy support; it follows redirects automatically and integrates with urllib.error exceptions — use urllib.request when you need redirect following and cookie handling without adding a dependency; use http.client directly when you need persistent connections for multiple requests to the same host or when you want to inspect raw response objects before body consumption. The Claude Skills 360 bundle includes http.client skill sets covering HttpResponse dataclass, http_conn() context manager, request()/get()/post()/put()/delete()/head() core methods, fetch()/fetch_json() one-shot helpers, fetch_with_retry() with exponential backoff, stream_response() generator, and ServiceHealth/health_check() for uptime monitoring. Start with the free tier to try HTTP/1.1 client patterns and http.client pipeline code generation.