Backends¶
zipwire uses pluggable HTTP backends to fetch byte ranges from remote
servers. Each backend is a thin wrapper around a popular HTTP library that
implements the SyncReader or AsyncReader
protocol.
Available backends¶
Library |
Class |
Mode |
HTTP |
Install extra |
|---|---|---|---|---|
urllib3 |
|
sync |
1.1 |
(included) |
httpx2 |
|
sync |
1.1, 2 |
|
httpx2 |
|
async |
1.1, 2 |
|
requests |
|
sync |
1.1 |
|
aiohttp |
|
async |
1.1 |
|
Choosing a backend¶
Default / no extra dependency - use
Urllib3Reader. urllib3 is already a dependency of zipwire.Async with aiohttp - use
AiohttpReader. Best choice if your project already depends on aiohttp.Sync + async from one library - install
httpx2and useHttpx2SyncReaderorHttpx2AsyncReader. Thehttpx2extra includes HTTP/2 support via theh2package. Whenh2is available, HTTP/2 is enabled automatically; passhttp2=Falseto force HTTP/1.1 orhttp2=Trueto require it.Requests integration - use
RequestsReaderif your project already usesrequestsand you want to share sessions, authentication, or retry configuration.
Passing an existing client¶
Every backend constructor accepts an optional pre-configured client or session. When you pass one, zipwire will not close it - you remain responsible for its lifecycle.
import aiohttp
from zipwire import AsyncRemoteZip
from zipwire.backends import AiohttpReader
async with aiohttp.ClientSession() as session:
reader = AiohttpReader(
"https://archive.example/data.zip", session=session
)
async with AsyncRemoteZip(reader) as rz:
data = await rz.read("file.txt")
Writing a custom backend¶
Implement the SyncReader or AsyncReader
protocol and pass an instance to SyncRemoteZip or
AsyncRemoteZip:
from collections.abc import Iterator
from zipwire import Headers, SyncReader, SyncRemoteZip
class MyReader:
"""Minimal SyncReader implementation."""
def __init__(self, url: str) -> None:
self._url = url
def head(self) -> Headers:
... # HEAD request, check Accept-Ranges, return headers
def read_range(
self,
offset: int,
length: int,
) -> tuple[bytes, Headers]:
... # GET with Range header, return (data, response_headers)
def stream_range(self, offset: int, length: int) -> Iterator[bytes]:
... # GET with Range header, yield chunks
def close(self) -> None:
... # release resources
assert isinstance(MyReader("..."), SyncReader) # runtime-checkable
See the SyncReader and AsyncReader
protocol definitions for the full method signatures.
Why only absolute byte ranges?¶
HTTP range requests support two forms: absolute ranges
(bytes=<start>-<end>) and suffix ranges (bytes=-<N>). Suffix
ranges request the last N bytes without requiring the client to know
the total file size. While RFC 7233 defines both forms, support varies
across CDNs and cloud storage services:
Service |
Suffix ranges |
Notes |
|---|---|---|
AWS S3 |
supported |
Full RFC 7233 compliance. |
Google Cloud Storage |
supported |
Confirmed by GCS team. |
Azure Blob Storage |
not supported |
Returns 200 with full object instead of 206. |
Amazon CloudFront |
not supported |
Returns 200 with full object instead of 206. |
Cloudflare R2 |
supported |
Via S3-compatible and Workers API. |
Fastly |
varies |
Some Fastly-fronted origins (e.g. PyPI) return 501. |
Cloudflare CDN |
uncertain |
Undocumented; may pass through to origin. |
Akamai |
likely supported |
General range support confirmed; suffix ranges undocumented. |
Because suffix ranges silently break on several major platforms (returning the full object or an error), zipwire uses only absolute byte ranges. On first access it sends a HEAD request to learn the file size, then computes absolute offsets for all subsequent range requests. This adds one round-trip compared to a suffix-range approach but works reliably everywhere.