Skip to content

Commit 94658e7

Browse files
committed
spec: issue-3549 browser cache for CAS downloads
Add a design spec for letting browsers cache CAS downloads: the download endpoint accepts the token in an Authorization header, sends ETag and Cache-Control headers, and answers conditional requests with 304 Not Modified without copying the object from the storage backend. Refs #3549 Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: 920bece2-38f4-43dd-9acc-d07e0325b568
1 parent bf4b1b4 commit 94658e7

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
owner: jiparis
3+
ticket: https://github.com/chainloop-dev/chainloop/issues/3549
4+
prd:
5+
---
6+
7+
# Spec issue-3549: Browser cache for CAS downloads
8+
9+
## Summary
10+
11+
The CAS download endpoint will let a browser keep a copy of a downloaded artifact and check that copy before each use. The endpoint will accept the download token in an `Authorization: Bearer` header, so that the download URL stays the same for a given digest. The response will tell the browser the digest of the content and how to cache it. When the browser already has the content, the CAS will answer "not modified" and will not copy the object from the storage backend. The Chainloop platform viewers need this change to show large artifacts, such as AI coding session transcripts, with no full download on each open.
12+
13+
## Problem
14+
15+
- Each open of the same artifact in a browser downloads it again in full. The CAS copies the object from the storage backend and checks its digest before it sends the first byte. For a large artifact, the user waits each time.
16+
- The download URL holds a new token in the query on each call. The browser cache uses the URL as its key, so it never finds a match.
17+
- The download response has no validator and no cache instructions. The browser cannot ask the CAS if its copy is still good.
18+
19+
## Goals and Non-Goals
20+
21+
- Goal: a browser that has a copy of an artifact gets a "not modified" answer with no content on the next open.
22+
- Goal: the CAS does not copy the object from the storage backend for a "not modified" answer.
23+
- Goal: the CAS checks the token on every request, also when the browser uses its copy.
24+
- Goal: the CLI and the download links that use the query token work as before.
25+
- Non-goal: a cache in the CAS or the controlplane. The cache is in the browser only.
26+
- Non-goal: changes to the CLI, the controlplane, or the CORS settings.
27+
- Non-goal: range requests or partial downloads.
28+
29+
## Requirements
30+
31+
### R-001: Token in the Authorization header
32+
The download endpoint MUST accept a token in an `Authorization: Bearer` header. When the header is present, the CAS MUST use the header token and MUST NOT use the query token. When the header is not present, the CAS MUST use the `t` query parameter, as before.
33+
- Done when: a request with a valid header token and no query token downloads the artifact. A request with only a valid query token works as before. A request with a bad header token and a valid query token gets a 401.
34+
35+
### R-002: Cache headers on the download
36+
A successful download MUST send `ETag` with the digest in quotes (for example `"sha256:abc..."`) and `Cache-Control: private, no-cache`.
37+
38+
### R-003: Not modified answer
39+
When the request has an `If-None-Match` value that matches the digest, the CAS MUST answer `304 Not Modified` with no content. The 304 MUST send the same `ETag` and `Cache-Control` headers as R-002. The CAS MUST NOT copy the object from the storage backend for a 304.
40+
- Done when: a request with a matching `If-None-Match` gets a 304 with no content, and the storage backend gets no download call.
41+
42+
### R-004: Checks before a 304
43+
Before it answers 304, the CAS MUST check the token. It MUST also check with a metadata call that the object exists in the storage backend. A bad or missing token MUST get a 401. An object that is not in the storage backend MUST NOT get a 304.
44+
45+
### R-005: No audit event for a 304
46+
A 304 answer MUST NOT record a download audit event. A 200 download MUST record the event as before.
47+
48+
## Constraints
49+
50+
- The repository is public. A CAS of an earlier version sends no cache headers, and clients must still work with it. They then download the content each time.
51+
- The CLI and the existing download links keep the query token and their current URLs.
52+
- The CORS preflight already allows the `Authorization` header and sends a 24-hour max age. A cross-origin viewer thus does not send a preflight on each open.
53+
54+
## Proposal
55+
56+
The browser viewer gets a download URL and a token from the controlplane, as today. It takes the token out of the URL and sends it in the `Authorization` header to `/download/<digest>`. That URL does not change, so the browser cache can find its copy. On the next open, the browser adds `If-None-Match` from its stored `ETag`.
57+
58+
The CAS reads the token from the header, or from the query when there is no header. It then does the checks that it does today: the token role, the storage backend, and a metadata call for the object. If `If-None-Match` matches the digest, the CAS stops here and answers 304. If not, it continues as today: it copies the object, checks its digest, and sends it with the new headers.
59+
60+
The `ETag` is the digest. The digest identifies the bytes, so the `ETag` never goes stale. `no-cache` makes the browser check with the CAS before each use, so the CAS checks access each time. `private` stops shared proxies from storing the content.
61+
62+
```mermaid
63+
sequenceDiagram
64+
participant B as Browser
65+
participant CAS
66+
participant S as Storage backend
67+
alt First open
68+
B->>CAS: GET /download/digest (Authorization)
69+
CAS->>CAS: Check token
70+
CAS->>S: Read metadata
71+
CAS->>S: Copy object, check digest
72+
CAS-->>B: 200, ETag, Cache-Control private no-cache
73+
else Next open
74+
B->>CAS: GET /download/digest (Authorization, If-None-Match)
75+
CAS->>CAS: Check token
76+
CAS->>S: Read metadata
77+
CAS-->>B: 304 Not Modified, ETag, Cache-Control
78+
end
79+
```
80+
81+
## Decision Record
82+
83+
| ID | Decision | Choice | Why (and what we rejected) | Source |
84+
|----|----------|--------|----------------------------|--------|
85+
| D-001 | Where to cache | The browser HTTP cache | The browser downloads straight from the CAS, so a cache there skips the backend copy and the transfer. Rejected: a server cache in the controlplane (the controlplane never sees the bytes, and its message size limit is too small). | drafting |
86+
| D-002 | How the browser checks its copy | `ETag` set to the digest, with `Cache-Control: private, no-cache` | The CAS checks access on each open, and a check costs no backend copy. Rejected: `max-age` with no check (a user who loses access keeps the copy until it expires). | drafting |
87+
| D-003 | How the URL stays the same | Token in an `Authorization` header | The browser cache uses the URL as its key, and the token changes on each call. Rejected: reuse one token for a user and a digest (the URL stays the same only for the life of the token). | drafting |
88+
| D-004 | Token source when both are present | The header wins | A client that sends a header sets its intent. A bad header fails, and the CAS does not fall back to the query. Rejected: reject a request with both (the viewer must then strip the query). Rejected: the query wins (the header has no effect for a URL that still holds a token). | drafting |
89+
| D-005 | Check that the object exists before a 304 | Yes, with a metadata call | A deleted object stops showing at once. The metadata call is cheap. Rejected: answer 304 from the digest alone (a browser can show a deleted object until it discards its copy). | drafting |
90+
| D-006 | Audit event for a 304 | None | The download event records bytes sent, and a 304 sends none. The first download from that browser already recorded the access. Rejected: the same event on each 304. | drafting |
91+
92+
## Open Questions
93+
94+
None.
95+
96+
## Risks
97+
98+
| Risk | Mitigation |
99+
|------|------------|
100+
| A browser keeps evidence on disk. | The copy is private to the browser, and the CAS checks the token before each use. |
101+
| The storage backend lost an object that a browser still holds. | The metadata call before a 304 finds the missing object, and the CAS does not answer 304. |
102+
| A shared proxy stores a download. | `Cache-Control: private` tells shared proxies not to store it. |

0 commit comments

Comments
 (0)