Summary
corsMiddleware() in internal/api/router.go unconditionally sets both:
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
These are mutually exclusive per the CORS spec. A credentialed response must specify an explicit origin, not the wildcard. Browsers reject any response that combines them.
Impact
The Nebi SPA's index.html (Vite default) ships:
<script type="module" crossorigin src="/assets/index-*.js"></script>
The crossorigin attribute forces a CORS check even for same-origin module loads. With the invalid header combo above, the browser silently refuses to load the JS bundle. The SPA never initializes, and users land in an infinite /login reload loop with no diagnostic surfaced unless they open DevTools.
Reproduces on Firefox and Chrome. Both same-origin and cross-origin module loads fail.
Symptom in Firefox console:
Loading failed for the module with source "https://<host>/assets/index-<hash>.js". login:16:71
The bundle itself is valid JS served with Content-Type: application/javascript and HTTP 200 — fetch() retrieves it successfully. Only <script type="module" crossorigin> loading fails, because that path enforces CORS.
Root cause
internal/api/router.go:489-505:
// corsMiddleware adds CORS headers
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Cache-Control", "no-store")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
The middleware applies to every route, including static /assets/* responses served by the SPA file handler.
Proposed fix
Two viable options:
Option A — drop credentials (simpler)
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
// no Access-Control-Allow-Credentials
The SPA's <script type="module" crossorigin> uses anonymous crossorigin mode (the default), so the browser never sends credentials with module loads anyway — the credentials header doesn't help that case. If Nebi exposes any cross-origin API surface that needs cookies/auth, option B is required instead.
Option B — echo the request origin (correct general pattern)
origin := c.Request.Header.Get("Origin")
if origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Vary", "Origin")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
With an allowlist if stricter behavior is wanted:
allowedOrigins := map[string]struct{}{
cfg.PublicURL: {},
// ...
}
if _, ok := allowedOrigins[origin]; ok {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Vary", "Origin")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
Either option resolves the browser-blocking bug. Option B is the correct pattern for any API that genuinely needs credentialed CORS; option A is fine if Nebi is only ever accessed from a same-origin SPA.
Adjacent issue: Cache-Control: no-store applied to static assets
While debugging this, I noticed the same middleware unconditionally emits Cache-Control: no-store on every response, including immutable hashed-filename bundles (/assets/index-<hash>.js). The inline comment explains the WebView/desktop concern, but the consequence is browsers re-download ~500KB of JS on every navigation even though the filename hash already guarantees cache-busting.
Worth scoping the no-store header to API responses (e.g. /api/*) and letting /assets/* be cacheable. Happy to file a separate issue if preferred — flagging here because the fix touches the same middleware function.
Repro
curl -sI https://<nebi-host>/assets/index-<any-hash>.js
Both Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true appear in the response. Then load https://<nebi-host>/login in any standards-compliant browser and observe the module-load failure in DevTools' console.
Summary
corsMiddleware()ininternal/api/router.gounconditionally sets both:These are mutually exclusive per the CORS spec. A credentialed response must specify an explicit origin, not the wildcard. Browsers reject any response that combines them.
Impact
The Nebi SPA's
index.html(Vite default) ships:The
crossoriginattribute forces a CORS check even for same-origin module loads. With the invalid header combo above, the browser silently refuses to load the JS bundle. The SPA never initializes, and users land in an infinite/loginreload loop with no diagnostic surfaced unless they open DevTools.Reproduces on Firefox and Chrome. Both same-origin and cross-origin module loads fail.
Symptom in Firefox console:
The bundle itself is valid JS served with
Content-Type: application/javascriptand HTTP 200 —fetch()retrieves it successfully. Only<script type="module" crossorigin>loading fails, because that path enforces CORS.Root cause
internal/api/router.go:489-505:The middleware applies to every route, including static
/assets/*responses served by the SPA file handler.Proposed fix
Two viable options:
Option A — drop credentials (simpler)
The SPA's
<script type="module" crossorigin>usesanonymouscrossorigin mode (the default), so the browser never sends credentials with module loads anyway — the credentials header doesn't help that case. If Nebi exposes any cross-origin API surface that needs cookies/auth, option B is required instead.Option B — echo the request origin (correct general pattern)
With an allowlist if stricter behavior is wanted:
Either option resolves the browser-blocking bug. Option B is the correct pattern for any API that genuinely needs credentialed CORS; option A is fine if Nebi is only ever accessed from a same-origin SPA.
Adjacent issue:
Cache-Control: no-storeapplied to static assetsWhile debugging this, I noticed the same middleware unconditionally emits
Cache-Control: no-storeon every response, including immutable hashed-filename bundles (/assets/index-<hash>.js). The inline comment explains the WebView/desktop concern, but the consequence is browsers re-download ~500KB of JS on every navigation even though the filename hash already guarantees cache-busting.Worth scoping the
no-storeheader to API responses (e.g./api/*) and letting/assets/*be cacheable. Happy to file a separate issue if preferred — flagging here because the fix touches the same middleware function.Repro
Both
Access-Control-Allow-Origin: *andAccess-Control-Allow-Credentials: trueappear in the response. Then loadhttps://<nebi-host>/loginin any standards-compliant browser and observe the module-load failure in DevTools' console.