Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions gradle-build-cache-http-status-codes/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gradle build cache HTTP status codes: 401, 403, 404 and 413</title>
<meta name="description" content="What 200, 201, 404, 401, 403 and 413 mean when Gradle talks to a self-hosted build cache server, tested on a real 0.2.1 server: passwords, the read-only login, and the entry size limit.">
<link rel="canonical" href="https://fosterstack.com/gradle-build-cache-http-status-codes/">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<style>
:root {
--bg: #ffffff; --fg: #16181d; --muted: #5b6472; --line: #e4e7ec;
--accent: #0d7a5f; --accent-fg: #ffffff; --card: #f6f8f9; --warn-bg: #fff7ed; --warn-line: #fdba74;
--mark: #0f4d66;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #101318; --fg: #e8eaf0; --muted: #98a2b3; --line: #262c36;
--accent: #2fbf95; --accent-fg: #08110e; --card: #171c23; --warn-bg: #241a10; --warn-line: #7c4a12;
--mark: #e8eaf0;
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg); color: var(--fg);
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
}
main { max-width: 44rem; margin: 0 auto; padding: 0 1.25rem 4rem; }
header.site { max-width: 44rem; margin: 0 auto; padding: 1.25rem; display: flex; align-items: baseline; gap: .6rem; }
.logo { font-weight: 700; font-size: 1.05rem; letter-spacing: -.01em; }
.logo span { color: var(--accent); }
.logo .mark { width: 1.2em; height: 1.2em; vertical-align: -.24em; margin-right: .4em; }
.tag { color: var(--muted); font-size: .85rem; }
.notice {
background: var(--warn-bg); border: 1px solid var(--warn-line); border-radius: 8px;
padding: .7rem 1rem; font-size: .92rem; margin: 1.5rem 0 2.5rem;
}
h1 { font-size: 2rem; line-height: 1.2; letter-spacing: -.02em; margin-bottom: 1rem; }
.lede { font-size: 1.12rem; color: var(--muted); margin-bottom: 2rem; }
h2 { font-size: 1.25rem; margin: 3rem 0 .9rem; letter-spacing: -.01em; }
p + p { margin-top: .8rem; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: .8rem; margin-top: 1rem; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 1rem 1.1rem; }
.card h3 { font-size: .98rem; margin-bottom: .35rem; }
.card p { font-size: .9rem; color: var(--muted); }
ul.trust { list-style: none; margin-top: .5rem; }
ul.trust li { padding: .45rem 0 .45rem 1.6rem; position: relative; }
ul.trust li::before { content: "✓"; position: absolute; left: .2rem; color: var(--accent); font-weight: 700; }
code, pre { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; }
code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: .1em .35em; }
pre code { background: none; border: 0; padding: 0; color: inherit; font-size: 1em; }
button {
padding: .7rem 1.3rem; font-size: 1rem; font-weight: 600; cursor: pointer;
background: var(--accent); color: var(--accent-fg); border: 0; border-radius: 8px;
}
button:disabled { opacity: .6; cursor: default; }
table { width: 100%; border-collapse: collapse; font-size: .92rem; margin-top: 1rem; }
th, td { text-align: left; padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
pre { background: #0f172a; color: #e2e8f0; padding: .9rem 1rem; border-radius: 8px;
overflow-x: auto; font-size: .9rem; line-height: 1.5; }
.cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
.btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
text-decoration: none; background: var(--accent); color: #fff; }
.btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
.muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
a { color: var(--accent); }
</style>
</head>
<body>
<header class="site">
<div class="logo"><svg class="mark" viewBox="0 0 96 96" aria-hidden="true" focusable="false"><rect x="12" y="14" width="72" height="18" rx="5" fill="var(--mark)"/><rect x="12" y="39" width="48" height="18" rx="5" fill="var(--mark)"/><rect x="12" y="64" width="26" height="18" rx="5" fill="var(--accent)"/></svg>foster<span>stack</span></div>
<div class="tag">Infrastructure worth keeping — and kept patched.</div>
</header>
<main>
<h1>Gradle build cache HTTP status codes: 401, 403, 404 and 413</h1>
<p class="lede">Gradle talks to the cache with plain web requests, and it does not fail the build when the cache says no. So the status code is often the only clue. This is what each one means for a FosterStack Cache server.</p>

<h2>The short table</h2>
<table>
<tr><th>Status</th><th>When you get it</th><th>What to do</th></tr>
<tr><td><strong>200</strong></td><td>A stored entry was found and sent.</td><td>Nothing. This is a cache hit.</td></tr>
<tr><td><strong>201</strong></td><td>An upload was stored.</td><td>Nothing.</td></tr>
<tr><td><strong>404</strong></td><td>That key is not stored.</td><td>Usually nothing. It is a cache miss. With a correct password it means the key just is not there yet.</td></tr>
<tr><td><strong>401</strong></td><td>No password, or the wrong one.</td><td>Fix the credentials block. See below.</td></tr>
<tr><td><strong>403</strong></td><td>A write sent with the read-only login.</td><td>Use the read-write login for machines that upload, or set the client not to upload.</td></tr>
<tr><td><strong>413</strong></td><td>An upload bigger than the server's size limit for one entry.</td><td>Raise <code>FSCACHE_MAX_BODY_BYTES</code> if the entry is meant to be cached.</td></tr>
</table>

<h2>Tested on a real server</h2>
<p>These were all run against a 0.2.1 server started with a read-write login, a read-only login, and the body limit set to 1024 bytes. Your numbers for the limit will differ, since the default is 1 GiB.</p>
<pre><code>GET /healthz (no credentials) 200
GET /metrics (no credentials) 200
GET key, no credentials 401
GET key, wrong password 401
GET missing key, read-write login 404
PUT 5 bytes, read-write login 201
GET stored key, read-write login 200
GET stored key, read-only login 200
HEAD stored key, read-only login 200
PUT, no credentials 401
PUT, read-only login 403
PUT 2048 bytes (limit set to 1024), read-write login 413
GET the too-large key afterwards 404
/statusz, no credentials 401</code></pre>
<p>The health and metrics paths stay open without a password, so monitoring works. The status page does not.</p>

<h2>401 is the quiet one</h2>
<p>Gradle treats a remote cache failure as non-fatal, so a 401 does not break the build and shows nothing at the default log level. The build just never uses the cache. Run with <code>-i</code> to see it. The usual causes are no <code>credentials</code> block, a password that did not reach Gradle's background daemon, or a typo. To test the server directly:</p>
<pre><code>curl -s -o /dev/null -w &#x27;%{http_code}\n&#x27; -u gradle:wrong-password https://cache.example.com/some-key</code></pre>
<p>A 401 here means the credentials are wrong. A 404 means they are right and the key simply is not stored. More steps are in <a href="/gradle-remote-cache-not-working/">Gradle remote build cache not working</a>.</p>

<h2>403 means the login worked but may not write</h2>
<p>The server can have an optional read-only login. A client holding it can read (GET and HEAD return 200 as normal) but any write gets a 403. That is the intended way to give laptops a cache they can read and not change. See <a href="/remote-build-cache-security/">who can write to a remote build cache</a>.</p>

<h2>413 and entry size</h2>
<p>A single upload over the limit is refused with 413 and nothing is stored, so a later request for that key is a 404. The limit is <code>FSCACHE_MAX_BODY_BYTES</code> and defaults to 1 GiB per entry.</p>

<p class="muted-note">Related: <a href="/build-cache-server-sizing/">server sizing</a> and <a href="/monitor-build-cache-prometheus-grafana/">monitoring</a>.</p>
<div class="cta-row">
<a class="btn" href="/gradle-remote-cache-not-working/">Gradle remote cache not working</a>
<a class="btn btn-secondary" href="/">FosterStack home</a>
</div>
</main>
<footer>
<div>
<p>FosterStack is not affiliated with, endorsed by, or sponsored by Gradle Inc.
"Gradle" and "Develocity" are trademarks of Gradle Inc., used here only to identify
compatibility. FosterStack contains no Gradle Inc. code; it is a clean-room implementation
of the documented remote build cache HTTP protocol. Apache Maven is a trademark of the
Apache Software Foundation; the Maven Build Cache Extension is Apache's project, not
ours.</p>
<p style="margin-top:.6rem">© 2026 FosterStack · <a href="/regulated/">For regulated markets</a> · <a href="https://github.com/fosterstack/cache">github.com/fosterstack/cache</a> · <a href="mailto:hello@fosterstack.com">hello@fosterstack.com</a></p>
</div>
</footer>
</body>
</html>
4 changes: 4 additions & 0 deletions sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://fosterstack.com/gradle-build-cache-http-status-codes/</loc>
<lastmod>2026-10-08</lastmod>
</url>
</urlset>
Loading