Skip to main content

A tiny, dependency-free HTTP client for Python with cancellable in-flight requests and hard wall-clock timeout.

Project description

cancellable_http_client

A tiny, dependency-free HTTP client for Python with cancellable in-flight requests and hard wall-clock timeout.

  • Standard library only — no requests, no httpx, no urllib3.
  • Single file, ~300 lines.
  • Synchronous API that plays well with threading-based workers.
  • Safe close() from any thread, at any time, including mid-transfer.
  • Hard wall-clock timeout that bounds the entire request.

Why it exists

Python has no clean way to interrupt a thread that is blocked on a socket read. concurrent.futures.Future.cancel() does nothing once the task has started, and requests / urllib.urlopen() give you no handle to abort an in-flight request.

The one primitive that does work is closing the underlying socket: any pending recv() immediately unblocks with an error. This module wraps that trick behind a tiny, boring API so you don't have to reinvent it — or worry about the lifecycle edge cases — every time you need it.

If your codebase is built around asyncio, you don't need this; use httpx and task.cancel() instead. This module targets the very real case where you have existing threaded code and you want one HTTP call in the middle of it to be cancellable, without rewriting everything to be async.

Usage

import time
import cancellable_http_client as client

req = client.Request("https://example.com/")
req.start()           # the actual TCP connection happens here
start = time.monotonic()
while not req.done:
    if time.monotonic() - start > 5:
        print("taking too long, aborting...")
        req.close()   # interrupts the request if it's still in-flight
    req.wait(0.1)     # wait a bit before checking again
if req.error:
    print(f"failed: {req.error}")
elif req.response and req.response.status == 200:
    print(req.response.body)
req.close()           # safe to call any time, even mid-flight

You can also use it as a context manager:

with client.Request("https://example.com/") as req:
    req.start()
    req.wait(timeout=5)
    ...
# close() is called automatically on exit

Sharing a thread pool

By default each Request spawns its own daemon thread. To reuse a pool instead, assign an Executor to the module-level attribute:

from concurrent.futures import ThreadPoolExecutor
client.executor = ThreadPoolExecutor(max_workers=8)

API

Request(url, method="GET", headers=None, body=b"", socket_timeout=30, timeout=None)

Construct a request. No network I/O happens here — connection failures are reported via error after start().

  • socket_timeout — per-socket-operation timeout in seconds, passed to http.client.HTTPConnection.
  • timeout — wall-clock limit in seconds for the entire request. Triggers close() automatically if the request is not done in time. None disables.
  • start() — kick off the request. Non-blocking.
  • wait(timeout=None) -> bool — block until the request finishes. Returns True on completion, False on timeout.
  • close() — abort the request and release resources. Safe to call any time, from any thread, any number of times.
  • done (property)True once the request has finished (success, failure, or close).
  • response — a Response object on success, otherwise None.
  • error — the exception raised during the request, or None.

Response

A read-only, socket-free container exposing the same attributes as http.client.HTTPResponse:

  • status, reason, version
  • headers (an http.client.HTTPMessage)
  • body (bytes, eagerly read)
  • getheader(name, default=None), getheaders()

Robust timeout

Most Python HTTP clients set a per-socket-operation timeout (socket.settimeout). This leaves several gaps:

  • Slow drip — a server that sends one byte every 29 seconds never triggers a 30-second socket timeout, yet the total transfer can take arbitrarily long.
  • DNS resolutionsocket.getaddrinfo() is a blocking C library call with no timeout parameter. Python cannot interrupt it.
  • Total elapsed time — there is no built-in way to cap the wall-clock time of an entire request across connection, TLS handshake, sending, and receiving.

cancellable_http_client addresses this with two separate knobs:

Parameter Scope Default
socket_timeout Per socket operation (connect, send, recv) 30 s
timeout Wall-clock limit on the entire request None (no limit)

When timeout fires it calls close(), which immediately unblocks any pending socket operation by closing the underlying connection. This gives you a hard upper bound on how long wait() will block — something that socket_timeout alone cannot guarantee.

Comparison with existing libraries

cancellable_http_client httpx requests
Cancel an in-flight request from another thread ⚠️ async, unreliable ⚠️ hacky
Hard wall-clock timeout on entire request ⚠️ per-operation ⚠️ per-operation
Synchronous API ✅ (also async)
No third-party dependencies
Line count ~300 thousands thousands
Fits a threading-based worker ⚠️
Redirects, cookies, User-Agent ⚠️ manual

httpx is a good choice if you are already in an asyncio world, though task.cancel() on in-flight requests can leave the connection pool in a broken state. requests does not offer a reliable way to interrupt an in-flight call; Session.close() does not forcibly close active sockets (psf/requests#5633).

Limitations

This library is a thin wrapper around http.client and does not provide the high-level conveniences found in requests or httpx:

  • No automatic redirect following
  • No cookie management
  • No default User-Agent header
  • No HTTP/2 support

These are all http.client limitations, not restrictions added by this library. You can still handle them manually via the headers parameter.

Tests

python -m unittest discover -s tests -v

No third-party test dependencies. Tests use local throwaway servers (normal, slow, blackhole, mid-body disconnect) to exercise cancellation, timeout, and error paths without touching the network.

License

Copyright 2026 Sakilabo Corporation Ltd. Licensed under the Universal Permissive License v 1.0 (UPL-1.0).


References (for the eventual public release)

Background reading and related work collected during the design of this module. Useful as citations in the README or a launch blog post.

Prior art / closest existing work

  • "TIL: Stopping Requests Mid Flight" — haykot.dev A blog post describing the "close the socket from another thread" trick as a personal discovery. The same underlying idea as this module, but kept as a snippet rather than packaged as a library.

  • httpcore on PyPI the low-level HTTP engine behind httpx. Supports cancellation via async task cancellation; relies on asyncio or trio.

  • HTTPX modern high-level HTTP client; cancellation is done via task.cancel() in an async context.

  • asyncio-cancel-token a cancellation-token utility for asyncio-based code.

The underlying Python pain points

Related stdlib primitives this module builds on

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cancellable_http_client-1.0.tar.gz (11.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cancellable_http_client-1.0-py3-none-any.whl (9.9 kB view details)

Uploaded Python 3

File details

Details for the file cancellable_http_client-1.0.tar.gz.

File metadata

  • Download URL: cancellable_http_client-1.0.tar.gz
  • Upload date:
  • Size: 11.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cancellable_http_client-1.0.tar.gz
Algorithm Hash digest
SHA256 2a4ea8f21ed3c519fe0a889d9e1e48ff8aec4c8a8aff7a0849947e51debb60eb
MD5 2348852caa7a9724d99c7558ac4b044b
BLAKE2b-256 2c1c7e6a717c04d4199481af1783d933a3a162ae9d0b3d0958af4d4aac25aa27

See more details on using hashes here.

Provenance

The following attestation bundles were made for cancellable_http_client-1.0.tar.gz:

Publisher: publish.yml on sakilabo/cancellable-http-client-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cancellable_http_client-1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for cancellable_http_client-1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f2a9c09c05e36eb54034eacbd86f10f540394266bb7d88cad6cf315ea88c32b2
MD5 c50ea0ca86f0aa99b8f71c1d8dc2cb1f
BLAKE2b-256 2a9ee241d302541c383d3304487d0088592b0bf09c6ae4f05fadd0e55c66c9d6

See more details on using hashes here.

Provenance

The following attestation bundles were made for cancellable_http_client-1.0-py3-none-any.whl:

Publisher: publish.yml on sakilabo/cancellable-http-client-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page