Skip to content

Add storage.cache_artifacts to serve artifacts without storing them - #387

Merged
andrew merged 4 commits into
git-pkgs:mainfrom
DANIILSKRIPCHENKO:feature/storage-passthrough
Oct 2, 2026
Merged

andrew merged 4 commits into
git-pkgs:mainfrom
DANIILSKRIPCHENKO:feature/storage-passthrough

Conversation

@DANIILSKRIPCHENKO

@DANIILSKRIPCHENKO DANIILSKRIPCHENKO commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Closes #385

Summary

Adds storage.cache_artifacts: false (PROXY_STORAGE_CACHE_ARTIFACTS). When enabled, artifact downloads are streamed from upstream to the client and never
written to storage or the cache database. Metadata filtering, cooldown and the denylist work as before.

Use case: running the proxy behind another cache (e.g. a remote repository in an artifact manager) purely for cooldown, without keeping a
second copy of every package.

Details

  • GetOrFetchArtifact and the URL-based fetch path skip the cache lookup, request coalescing and storeArtifact. They stream the upstream
    body directly. GetCachedArtifact returns nothing.
  • Artifacts whose digest is known up front (OCI blobs, Swift archives, Helm charts) are checked while they stream. These responses are sent
    chunked. On a digest mismatch the read fails and the handler aborts the connection, so the client never gets a tampered artifact as a complete
    response.
  • Config validation rejects passthrough together with scanning.enabled, storage.direct_serve or mirror_api, since all three need stored
    artifacts. The mirror command refuses to run with it.
  • Storage and the database are still required: metadata caching, health checks and cooldown publish times use them.

Tests

  • Artifacts are streamed and not stored (nothing in storage or the DB). Existing cached entries are ignored.
  • Digest verification: a matching digest streams the artifact, a mismatch fails the read, and ServeArtifact aborts with no Content-Length.
  • npm download cooldown in passthrough mode: a version older than the window is served, a fresh one gets 404 and is never fetched.
  • Config validation for each combination that is not allowed, plus the env var.

go test ./..., go vet ./... and golangci-lint run ./... pass.

With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.

Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.

Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@andrew andrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues need fixing before merging:

  1. serveArtifact only aborts on ErrArtifactDigestMismatch, so other upstream read errors produce successful truncated responses. I reproduced this through the running proxy with an OCI blob whose upstream declared Content-Length: 100 but sent five bytes, and an npm tarball whose upstream closed a chunked response without the final chunk. Normal mode returned 502 for both. Passthrough returned 200 with Content-Length: 5, body short, and no client read error. The OCI response also retained the expected digest header without completing verification. Abort on any copy error and add HTTP tests asserting that clients see an incomplete response for both cases.

  2. Swift archive HEAD requests call getCachedArtifactWithUpstreamHash directly, bypassing the passthrough guard in GetCachedArtifact. I cached a Swift archive in normal mode, restarted with passthrough enabled, and made the upstream archive return 404 while keeping its release metadata available. HEAD still returned the cached 200 and length without requesting the upstream archive; GET returned 404. Apply the passthrough guard to this cache path too and add a regression test covering an existing cache entry when passthrough is enabled.

Comment thread config.example.yaml Outdated
# filtering, cooldown and the denylist still apply. Useful when another
# cache sits in front of the proxy. Incompatible with direct_serve,
# scanning and mirror_api.
passthrough: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small naming nit: passthrough is a bit ambiguous for this project. Since the whole point of the proxy is filtering, "passthrough" can easily be read as "pass everything through untouched", i.e. no cooldown and no denylist, which is the opposite of what this mode does.
passthrough is also a bit misleading in the storage section. Storage and the database stay in use: metadata is still cached there, and so are the publish times cooldown needs. Only artifacts are no longer stored.

What about naming it after what actually changes, e.g. storage.cache_artifacts (default true, PROXY_STORAGE_CACHE_ARTIFACTS)? An enum would also work, if that fits the config style better: storage.artifacts: cache | passthrough. Not a blocker, happy to go with whatever you prefer.

DANIILSKRIPCHENKO and others added 2 commits October 2, 2026 14:31
- serveArtifact aborts the response on any body read error, and when fewer
  bytes than the declared size were written, not only on a digest mismatch.
  A failed or short upstream body in passthrough mode no longer reaches the
  client as a complete 200.
- Streamed upstream read failures are logged and counted as
  stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
  read path, including Swift archive HEAD requests, ignores entries stored
  before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
  PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.

Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DANIILSKRIPCHENKO DANIILSKRIPCHENKO changed the title Add storage.passthrough to serve artifacts without storing them Add storage.cache_artifacts to serve artifacts without storing them Oct 2, 2026
Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DANIILSKRIPCHENKO

DANIILSKRIPCHENKO commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — both issues are fixed.

  1. Truncated upstream bodies (0a3ec11): serveArtifact now aborts on any read error, and when fewer bytes than the declared size were written, not only on a digest mismatch. Upstream read failures are logged and counted as stream_failed. Added HTTP tests through the handlers for an OCI blob shorter than its Content-Length and an npm tarball whose chunked response ends without the final chunk — both now reach the client as incomplete responses.

  2. Swift archive HEAD (0a3ec11): the guard moved into checkCache, so every cache read path misses when artifact caching is off. Denylisted versions are still rejected. Added a regression test with an archive cached in normal mode, then HEAD and GET with caching disabled and the upstream archive returning 404.

@Markeli good call on the name — renamed to storage.cache_artifacts (default true, PROXY_STORAGE_CACHE_ARTIFACTS). The internal Proxy.Passthrough field is now StreamArtifacts (e8d26af); its zero value keeps caching on for any Proxy built outside the server config, such as mirror and tests.

I also merged current main into the branch.

@andrew
andrew merged commit b40a50a into git-pkgs:main Oct 2, 2026
6 checks passed
@andrew

andrew commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: pass-through mode without artifact storage

3 participants