I was adding auth to the keryx studio, the little browser UI where I review a reel’s takes before anything gets posted. The plan was the obvious one: the server mints a random bearer token at startup, prints it once, and every request has to carry it. My own framework already had the middleware. Ten minutes of wiring, tops. Then I read the middleware before wiring it, and stopped.
The header a browser refuses to send
Bearer tokens live in the Authorization header, and the middleware in go-tool-base extracts credentials from headers. Which is fine for fetch() calls, the JavaScript can attach whatever it likes. But not everything the studio loads goes through fetch(). It’s a page full of <img>, <audio> and <video> tags, every take and preview pulled straight from a src attribute pointing at /api/v1/workspace/{slug}/file/....
And a browser will not attach your Authorization header to an <img> tag. There’s no attribute for it, no polite workaround; that’s simply not how src fetches work. And those media URLs live under /api/, so the bearer middleware gates them along with everything else: the fetch() calls carry their token and work, the page shell loads, and every single image comes back 401. A studio for reviewing your media that shows you exactly none of it. Nice.
The satisfying part: this never actually happened. No broken page, no debugging session at midnight. It fell out of reading the dependency’s API before writing the first line against it, which sounds virtuous and mostly means I’ve been burned before.
Two fixes I didn’t like
The obvious patches were sitting right there. Option one: studio-local glue, some cookie-or-query-string handling bolted into keryx in front of the framework’s middleware. Option two: append ?token=... to every media URL the studio renders.
Neither survived contact. The query-string version smears the credential across every URL, where it leaks into logs and copy-pasted links (a token in a URL is a secret with a publicist). And the app-local glue means my application grows its own private auth layer in front of the framework’s auth layer, which is two places for bugs to disagree with each other.
So I went with the third option, the one that wasn’t on the list: if the framework’s middleware only understands headers, teach the framework about cookies. It’s my framework. That’s the whole point of owning the thing.
A cookie the framework understands
go-tool-base v0.24.0 gained a cookie verifier as a first-class option on the same middleware:
func WithCookieVerifier(cookieName string, v authn.Verifier) AuthOption {
return func(c *authConfig) { c.cookieName = cookieName; c.cookie = v }
}
(pkged at v0.24.0.) The cookie is deliberately the ambient credential: if a request carries an explicit Authorization header, the header always wins. Cookies ride along on <img> fetches for free, because riding along on requests is the one thing cookies have always done. The keryx side then composes it like any other option:
authMW, err := gtbhttp.AuthMiddleware(
gtbhttp.WithBearerVerifier(v),
gtbhttp.WithCookieVerifier(sessionCookie, v),
gtbhttp.WithAuthSkipper(func(r *http.Request) bool { return !strings.HasPrefix(r.URL.Path, "/api/") }),
gtbhttp.WithAuthLogger(log),
)
(keryx at e3f2bf1.) The token itself is 32 bytes from crypto/rand, minted per run, never written to disk. The cookie is HttpOnly and SameSite=Strict, though not Secure, because this runs over plain http on a LAN and I’d rather document that limitation than pretend TLS exists where it doesn’t. Localhost stays open, only /api/* is gated, and if the gate can’t be built at startup the server refuses to start at all. No gate, no server… I’d sooner it fell over at boot than came up with the door propped open.
The only 401 fired on purpose
The end-to-end test binds the studio to a non-loopback address, hits the API without the token, and asserts the 401 actually fires; then again with the token, asserting the 200. The failure mode I spotted in a spec review now exists in exactly one place, deliberately, in a test, proving the gate is real.
No grand moral here, just a small habit worth naming: the ten minutes spent reading AuthMiddleware’s source before calling it is the cheapest debugging session I’ve ever had. The alternative timeline, the one where I wire it up first, is me squinting at a page of broken image icons wondering what on earth I’d broken. And the answer, of course, would have been that I hadn’t broken a thing. The code was fine. The whole design was wrong.
I prefer this timeline. The images load, the gate holds, and the framework walked away with a feature every future tool of mine gets for free.





