Nexus 0.1.5 shipped on April 13, 2026 with three fixes across the Bridge and Broker. None of them are flashy, but all three address failure modes that would have caused real production problems — incomplete JSON responses, panicking token retrievals for non-OAuth2 providers, and a reconnection loop that could not be tested or extended. This post walks through what each fix actually changed and why it matters.
Bridge: The goto Was the Wrong Tool for a Retry Loop
The MaintainGRPCConnection function in the Bridge was using a goto Retry jump to restart the connection loop after a failure. Functionally, a goto can implement a retry loop. The problem is what it prevents.
In Go, a goto cannot jump forward over a variable declaration. That means any variable you want to declare inside the retry body — for tracking attempt count, computing backoff duration, or holding per-attempt context — has to be declared before the label or lifted out to the function scope. The code gets increasingly contorted as you add logic. More seriously, if the goto jumps across a defer statement, the deferred call can be skipped entirely, which is the kind of bug that only appears under specific error paths.
PR #31 replaced the goto with a standard for loop that actually does what a retry loop should do:
- Exponential backoff: each failure doubles the wait duration, capped at
MaxBackoff. - Jitter: a random offset is applied to each wait period to prevent thundering herd if multiple Bridge instances restart simultaneously after a Gateway outage.
- Context-aware wait: the sleep uses
selectoverctx.Done()andtime.After, so a context cancellation during backoff exits immediately rather than waiting out the full sleep duration. ErrInteractionRequiredsentinel: when the Gateway returns a409 attention_requiredresponse (meaning the user needs to re-authenticate), the loop exits cleanly rather than retrying indefinitely. This is the correct behaviour for a permanent state error.PermanentErrortype: any error wrapped as aPermanentErrorstops the loop immediately, allowing callers to signal that a failure is not transient.
The refactor also shipped with six unit tests covering clean exit, permanent error, interaction required, retry-then-succeed, context cancellation during backoff, and exponential backoff growth verification. The old goto loop had zero tests — the control flow made it effectively untestable.
Broker: Every JSON Response Was a Partial-Write Risk
The Broker was writing all HTTP responses using json.NewEncoder(w).Encode(data). This is the idiomatic Go pattern for JSON responses, but it has a property that makes it unsafe as a production response strategy: it writes directly to the http.ResponseWriter as it encodes, which implicitly commits the HTTP headers and status code on the first write.
The consequence is that if encoding fails partway through — because the response struct contains an unserializable value, or because the encoder runs out of memory mid-stream — the client has already received a 200 OK status line and the beginning of a valid JSON body. The server cannot go back and send a 500. The client sees a truncated response that looks like a network interruption, not an error it can handle.
PR #32 introduced a WriteJSON helper in nexus-broker/pkg/httputil:
// WriteJSON marshals v to a buffer first, then writes atomically.
// If marshal fails, a 500 is sent before any bytes reach the client.
func WriteJSON(w http.ResponseWriter, status int, v any) {
b, err := json.Marshal(v)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(b)
}
All 15 call sites in the Broker were migrated from json.NewEncoder(w).Encode() to httputil.WriteJSON(). The Gateway's remaining streaming call site received the same treatment. If marshalling fails, the client now gets a proper 500 instead of a silent partial body.
Broker: SQL NULL Values for Non-OAuth2 Providers
This is the one that would have made api_key and basic_auth providers completely unusable in production. The Broker's profile store was using plain Go string fields for token_url, client_id, and client_secret in its database scan structs. For OAuth2 providers, those columns always have values. For API key and basic auth providers, they are NULL in the database — those fields are not part of their auth model.
When sqlx tried to scan a NULL database column into a plain string field, it panicked. The fix was to migrate those fields to sql.NullString across the affected structs — token_url, client_id, client_secret, and code_verifier in the consent state row — and then access their .String values at each use site.
The fix also added conditional injection for the OAuth token exchange body. Previously, client_id and client_secret were always added to the POST body fields, even as empty strings. Now they are only added when non-empty, which is the correct behaviour for credential strategies where those fields genuinely do not apply.
After this fix, API key and basic auth providers resolve tokens correctly. Before it, any token retrieval for a non-OAuth2 provider was a guaranteed panic.
Upgrade
Pull the v0.1.5 tag. The Bridge changes affect MaintainGRPCConnection callers — the function signature is unchanged, but the retry behaviour is now exponential rather than immediate. If you are monitoring reconnection timing, expect longer gaps between attempts on repeated failures, which is the intended behaviour. No database migrations are required.
Release notes are on GitHub.