HTTP Cache (harp_apps.http_cache)

HTTP Cache application for HARP - hishel 1.0 adapter.

class AsyncCacheTransport[source]

Bases: AsyncCacheTransport

async handle_async_request(request)[source]

Wraps the request to use a rewritten url (for cache key handling).

Parameters:

request (Request)

Return type:

Response

async request_sender(request)[source]

Unwraps the request before sending it, and drops the upstream’s connection-specific fields.

This is the last point at which Connection can still be read. Further down, hishel removes Connection itself and leaves the fields it named behind, so by the time the proxy controller sees a cached response there is nothing left to identify them by and RFC 9110 §7.6.1 cannot be applied. Dropping them here also means they are never written to the cache, which is what RFC 9111 §3.1 asks for.

content-length is deliberately kept: it describes the body being stored, and the controller drops it on the way out to the client anyway.

Parameters:

request (WrappedRequest)

Return type:

Response

class AsyncStorage[source]

Bases: AsyncBaseStorage

HARP’s AsyncBaseStorage implementation using blob storage backend.

This implementation adapts hishel 1.0’s Entry-based API to work with HARP’s blob storage system. We store a single entry per cache key, maintaining backward compatibility with existing cached data.

hishel addresses entries by UUID while this store addresses blobs by cache key, so a bounded index of recently seen ids bridges the two. See KEY_INDEX_SIZE for why a small bound is sufficient, and _key_for() for what happens when it is not.

__init__(storage, ttl=None, check_ttl_every=60, allow_heuristics=False)[source]
Parameters:
async close()[source]

Close the storage (required by AsyncBaseStorage interface).

Return type:

None

async create_entry(request, response, key, id_=None)[source]

Create and store a new cache entry.

Args:

request: The HTTP request response: The HTTP response key: The cache key id_: Optional UUID for the entry (generated if not provided)

Returns:

The created Entry

Parameters:
  • request (Request)

  • response (Response)

  • key (str)

  • id_ (UUID | None)

Return type:

Entry

async get_entries(key)[source]

Retrieve all entries for a given cache key.

Note: Our implementation stores only one entry per key, so this returns a list with at most one element.

Args:

key: The cache key

Returns:

List of Entry objects (empty if not found, single element if found)

Parameters:

key (str)

Return type:

List[Entry]

async remove_entry(id)[source]

Remove an entry by its ID.

hishel invalidates the stored entries a revalidation did not match, which only arises where several entries share a cache key. This store holds one entry per key, so hishel does not currently reach this method: the lists it builds for invalidation (revalidating_entries[:-1], and the non-matching entries after a 304) are always empty here. It is implemented rather than left inert because that is a property of the storage shape today and not of the contract, and it changes the moment one key can hold several variants. See https://github.com/msqd/harp/issues/910.

Args:

id: The entry UUID

Parameters:

id (UUID)

Return type:

None

async update_entry(id, new_entry)[source]

Update an existing entry by its ID.

hishel calls this to write a 304’s refreshed headers back onto the stored entry. If it does nothing, the entry stays exactly as stale as it was and every subsequent request revalidates again, without end.

Args:

id: The entry UUID new_entry: Either a new Entry object or a callable that transforms the existing entry

Returns:

The updated Entry, or None if it could not be resolved

Parameters:
Return type:

Entry | None

class AsyncStorageAdapter[source]

Bases: object

Adapter that serializes/deserializes Entry objects to/from HARP blob storage.

This maintains backward compatibility with the YAML serialization format while adapting to hishel 1.0’s Entry-based model.

__init__(storage)[source]
Parameters:

storage (IBlobStorage)

async retrieve_entry(key)[source]

Retrieve an Entry object from blob storage.

Args:

key: The cache key

Returns:

The Entry if found, None otherwise

Parameters:

key (str)

Return type:

Entry | None

async store_entry(key, entry)[source]

Store an Entry object in blob storage.

Args:

key: The cache key entry: The Entry to store

Returns:

The stored Blob

Parameters:
  • key (str)

  • entry (Entry)

Return type:

Blob

class WrappedRequest[source]

Bases: Request

A request wrapper that allows selective attribute overrides while preserving the original.

WrappedRequest extends hishel’s Request class to support overriding specific attributes (method, url, headers, stream, metadata) while maintaining access to the original wrapped request. This is particularly useful for cache key normalization in load-balanced scenarios where different backend URLs should share the same cache entries.

The wrapped request can be retrieved via unwrap() for actual network transmission, while the WrappedRequest itself (with overridden attributes) is used for cache operations.

This class is designed to work with dataclasses.replace(), which hishel uses during cache revalidation to add conditional headers (If-None-Match, If-Modified-Since). The wrapped request reference is stored in metadata to survive replace() operations.

Example:
>>> original_request = Request(method="GET", url="http://backend1.local/api/users")
>>> wrapped = WrappedRequest(original_request, url="http://normalized-endpoint/api/users")
>>> wrapped.url  # Returns normalized URL for cache key
"http://normalized-endpoint/api/users"
>>> wrapped.unwrap().url  # Returns original URL for transmission
"http://backend1.local/api/users"

Initialize a wrapped request with optional attribute overrides.

This constructor supports two modes: 1. Normal mode (request provided): Wraps the given request with optional overrides 2. Replace mode (request=None, all fields provided): Called by dataclasses.replace()

Args:

request: The original Request to wrap (None when called from replace()) method: Optional method override (defaults to wrapped.method) url: Optional URL override (defaults to wrapped.url) headers: Optional headers override (defaults to wrapped.headers) stream: Optional stream override (defaults to wrapped.stream) metadata: Optional metadata override (defaults to wrapped.metadata)

__init__(request=None, /, *, method=None, url=None, headers=None, stream=None, metadata=None)[source]

Initialize a wrapped request with optional attribute overrides.

This constructor supports two modes: 1. Normal mode (request provided): Wraps the given request with optional overrides 2. Replace mode (request=None, all fields provided): Called by dataclasses.replace()

Args:

request: The original Request to wrap (None when called from replace()) method: Optional method override (defaults to wrapped.method) url: Optional URL override (defaults to wrapped.url) headers: Optional headers override (defaults to wrapped.headers) stream: Optional stream override (defaults to wrapped.stream) metadata: Optional metadata override (defaults to wrapped.metadata)

Parameters:
unwrap()[source]

Return the request to actually send upstream.

The split is by who changed what. method and url come from the wrapped request, because those are the attributes this class overrides for cache-key normalization and the origin must be addressed as it really is. headers and stream are taken as they stand now, because hishel changes those.

That second half is what makes revalidation work. hishel builds the conditional request with dataclasses.replace(request, headers={..., "if-none-match": ...}), and replace() carries this object’s metadata through untouched, so the wrapped request held in that metadata is the request as it was before the conditional headers existed. Returning it sends the revalidation with no validator at all: the origin has nothing to compare against, cannot answer 304, and transfers the whole body again.

Returns:

A plain Request addressed at the real origin, carrying the current headers and body.

Return type:

Request

Submodules