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

Urllib3Reader

sync

1.1

(included)

httpx2

Httpx2SyncReader

sync

1.1, 2

httpx2

httpx2

Httpx2AsyncReader

async

1.1, 2

httpx2

requests

RequestsReader

sync

1.1

requests

aiohttp

AiohttpReader

async

1.1

aiohttp

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 httpx2 and use Httpx2SyncReader or Httpx2AsyncReader. The httpx2 extra includes HTTP/2 support via the h2 package. When h2 is available, HTTP/2 is enabled automatically; pass http2=False to force HTTP/1.1 or http2=True to require it.

  • Requests integration - use RequestsReader if your project already uses requests and 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.