Rules and patterns for making external HTTP calls in uhc-auth-proxy.
All outbound HTTP calls go through a single path:
requests/client/wrapper.go— shared HTTP client, auth headers, metrics, error handlingrequests/cluster/cluster.go— account management service facadeserver/server.go— maps upstream errors to HTTP responses, caches identity results
All external URLs are configured via environment variables with defaults set in init() functions:
| Variable | Default | Package |
|---|---|---|
CURRENT_ACCOUNT_URL |
https://api.openshift.com/api/accounts_mgmt/v1/current_account |
cluster |
GET_ACCOUNTID_URL |
https://api.openshift.com/api/accounts_mgmt/v1/cluster_registrations |
cluster |
ACCOUNT_DETAILS_URL |
https://api.openshift.com/api/accounts_mgmt/v1/accounts/%s |
cluster |
ORG_DETAILS_URL |
https://api.openshift.com/api/accounts_mgmt/v1/organizations/%s |
cluster |
ACCESS_TOKEN_URL |
https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token |
client |
TIMEOUT_SECONDS |
(none — must be set via env var) | client |
Use viper.GetString() to read endpoint URLs at call time rather than storing them at init time. This allows tests and deployments to override values after init. Note that TIMEOUT_SECONDS is read once at package load time (in the var block of wrapper.go, before viper defaults apply) and cannot be changed at runtime.
All outbound HTTP calls to the account management API go through the client.Wrapper interface:
type Wrapper interface {
Do(req *http.Request, label string, cluster_id string, authorization_token string) ([]byte, error)
}Rules:
- Never call
http.Client.Do()directly from business logic. Always go through aWrapper. - The
labelparameter is used for Prometheus metrics, not routing. Pass the URL string (e.g., the value returned byviper.GetString("CURRENT_ACCOUNT_URL")), not the variable name. Do()adds Authorization, Accept, and Content-Type headers automatically. Do not set these on the request before callingDo().- The Authorization header uses the format
AccessToken {cluster_id}:{authorization_token}. This is not a Bearer token — it is specific to the accounts management API.
A single package-level http.Client is shared across all requests in requests/client/wrapper.go.
Rules:
- Do not create additional
http.Clientinstances. Use the shared client. - The timeout is set at package load time from
TIMEOUT_SECONDS. There is no retry logic anywhere in the codebase. If a request fails, it fails. - There is no custom Transport, connection pooling config, or TLS settings — the Go defaults apply.
wrapper.Do() returns ([]byte, error) where both can be non-nil simultaneously:
- Status >= 400: Returns the response body AND a
*client.HttpErrorwithStatusCodeandMessage. - Transport error: Returns
nilbody and the underlying error.
Rules:
- When
wrapper.Do()returns an error, always check whether the body ([]byte) is also non-nil. A non-nil body on error means the upstream returned a structured error response. - Use
errors.As(err, &httpError)to extract the status code from*client.HttpError. - Default to HTTP 401 when the error is not an
HttpError.
When GetCurrentAccount receives both a body and an error, it attempts to unmarshal the body into *cluster.AccountError:
if b != nil {
res := &AccountError{}
if json.Unmarshal(b, res) == nil {
res.Inner = err
return nil, res
}
}Rules:
AccountErrorwraps the originalHttpErrorvia itsInnerfield and implementsUnwrap(). Useerrors.Asto reach either layer.- When logging an
AccountError, usezap.Object("account_error_verbose", accErr)— it implementszapcore.ObjectMarshaler. - Any new external call that returns structured error JSON should follow this same pattern.
There is a latent nil-pointer risk in wrapper.go: resp.StatusCode is accessed before the err != nil check. If the HTTP call fails with a transport error (DNS failure, timeout), resp will be nil and this line will panic. Any fix must move the metric increment to after the nil check.
- Successful identity lookups are cached for 2 hours in
cache/cache.go. Cache key is{cluster_id}:{authorization_token}. - Cache is checked before calling
GetIdentity. If the cache returns non-nil, no external call is made. - Only cache successful results. Errors are never cached.
- Call
cache.Clear()at the start of every test that exercises the handler.
requests/client/access.go manages OAuth tokens for the SSO endpoint:
- Tokens are cached in a package-level variable with a mutex.
- A token is refreshed when expired (
now >= expires) or when the cached token string is empty. - This is a
refresh_tokengrant type flow against Red Hat SSO.
| Fake | Purpose |
|---|---|
FakeWrapper |
Returns a preconfigured *Account response |
ErrorWrapper |
Always returns an error with nil body |
ErrorWithBodyWrapper |
Returns both a JSON body and a *client.HttpError |
When testing error paths that involve structured error bodies, use ErrorWithBodyWrapper and set both AccountError and StatusCode.
Call cache.Clear() in BeforeEach blocks. The global in-memory cache persists across test cases.
Verify error chain with errors.As or errors.Unwrap, not string matching:
unwrap := errors.Unwrap(err)
Expect(unwrap).To(BeAssignableToTypeOf(&AccountError{}))Requests must have a user-agent matching {operator-prefix}/{version} cluster/{cluster_id}. The recognized operator prefixes are defined in server/server.go.
Any new external call must record request duration and status code using the existing promauto registration pattern. Use the uhc_auth_proxy_ prefix for all metric names.