Proxying

A repository is most useful as a build's single front door: it serves your own artifacts and, on a miss, fetches the public ones from upstream, stores them, and serves them from then on. Jenesis Repository does this per format - the Maven layout can pull through from Maven Central, the OCI registry from Docker Hub - once you tell it where upstream is. Nothing is proxied until you do.

Pointing a format upstream

One setting per format names its upstream, keyed by the format id:

JENREG_PROXY_MAVEN=https://repo1.maven.org/maven2/
JENREG_PROXY_OCI=https://registry-1.docker.io/

With JENREG_PROXY_MAVEN set, http://localhost:8080/repository/maven/ resolves everything on Maven Central as well as what you published, so a build needs one <mirror> entry. With JENREG_PROXY_OCI set, docker pull localhost:8080/library/debian fetches the image through your server. An upstream must be https; the server warns loudly at boot about one that is not.

How a miss becomes a local hit

A GET or HEAD is served locally first. When that is a 404 and the format has an upstream, the format maps the request to the upstream URL and fetches it. An immutable artifact - a jar, a POM of a released version, an image layer - is stored content-addressed as it streams through, and served. The next request for it is a plain local hit that never touches the network again. The copy is a stream, digest and all, so a multi-hundred-megabyte layer is mirrored in a small, fixed heap.

A mutable index - a maven-metadata.xml, a tag list - is never cached that way, because it changes upstream. It is fetched fresh on each request, so an artifact published upstream after your first look shows through. To avoid re-downloading an index that has not changed, the fetcher revalidates it: it remembers the ETag or Last-Modified and sends a conditional request, and a 304 Not Modified answers from the remembered bytes. The upstream is still asked every time; only the transfer is saved.

The negative cache

A build tool makes a flood of requests for things that are not upstream at all: a version range it probes, a missing SNAPSHOT, an optional classifier, a .sha256 a client guesses at. Re-asking upstream for each one multiplies load and can trip its rate limit. So a definite upstream 404 is remembered for a short window and answered from memory.

Only a definite 404 is cached. A transport failure or an auth challenge (401, 403) is not, since it is transient or resolvable, and every success passes through untouched. An entry expires after jenreg.proxy-miss-ttl - one minute by default - so a newly published artifact is picked up within that window.

Verifying what upstream sent

Where upstream publishes a digest, the fetched bytes are held to it before they are stored. A Maven artifact is checked against the .sha1 file published beside it, and a mismatch is refused rather than cached. The OCI mirror verifies every blob against the sha256: digest that addresses it, and a manifest against the digest the upstream registry reports.

That matters because the alternative is worse than a failed download. A proxy that stores whatever upstream returned turns one bad response - a corrupted mirror, a tampered hop - into a durable local artifact that every later client receives. Refusing at the point of fetch keeps a bad byte from becoming the repository's own answer.

The OCI mirror

The OCI format follows the Distribution bearer-token handshake an upstream registry demands: a 401 with a Bearer challenge is exchanged for a token and the fetch is retried. It resolves multi-architecture image indexes to the manifests they list, and a mirrored layer dedupes against everything else the repository holds, because an OCI digest is the store's own key.

The fetcher module

All upstream traffic - a proxy fetch, an import, a revalidation - goes through one HTTP fetcher, which is a discovered module like every other capability. Without it the server still runs: it serves only what it holds, and an import is refused with 501. An installed fetcher can be switched off with jenreg.http=false, which has the same effect.

Selecting a fetcher by name with jenreg.fetcher=<name> is different from leaving the choice to discovery. A named fetcher that no installed module answers to fails the boot, because an operator who named a transport and silently got none would see every proxy route answer 404 as if upstream held nothing.

A single fetch is bounded by jenreg.proxy.request-timeout, one minute by default, so a hanging upstream cannot hold a request open indefinitely. This one key is read as a system property (-Djenreg.proxy.request-timeout=PT30S), not from the environment.

Settings

Key Default Effect
jenreg.proxy.<format> (unset - no proxying) The upstream URL for a format: jenreg.proxy.maven, jenreg.proxy.oci, jenreg.proxy.raw.
jenreg.proxy-miss-ttl 60s How long an upstream 404 is remembered; ISO-8601 (PT90S) or 90s / 5m; 0 disables the negative cache.
jenreg.proxy.request-timeout PT1M Per-request upstream timeout, ISO-8601 or plain seconds. System property only.
jenreg.http true Switch the HTTP fetcher off; the server then serves local content only.
jenreg.fetcher (discovered) Select a fetcher by name; a name nothing answers to fails the boot.

Leave the miss window at its default unless an upstream publishes very frequently and you need a miss re-checked sooner. Lowering it trades a little more upstream traffic for faster pickup of a just-published artifact; raising it shields a rate-limited upstream from a build tool's probing.