gotify/server · error
failed to get user info: %w
Error message
failed to get user info: %w
What it means
After a successful token exchange, ExternalTokenHandler calls `rp.Userinfo` to fetch claims about the end user from the provider's userinfo endpoint. If that call fails, the handler wraps the cause as 'failed to get user info: %w' and returns 500.
Source
Thrown at api/oidc.go:392
return
}
session, ok := a.popPendingSession(req.State)
if !ok {
ctx.AbortWithError(http.StatusBadRequest, errors.New("unknown or expired state"))
return
}
exchangeOpts := []rp.CodeExchangeOpt{
rp.CodeExchangeOpt(rp.WithURLParam("redirect_uri", session.RedirectURI)),
rp.WithCodeVerifier(req.CodeVerifier),
}
tokens, err := rp.CodeExchange[*oidc.IDTokenClaims](ctx.Request.Context(), req.Code, a.Provider, exchangeOpts...)
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, fmt.Errorf("token exchange failed: %w", err))
return
}
info, err := rp.Userinfo[*oidc.UserInfo](ctx.Request.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), a.Provider)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("failed to get user info: %w", err))
return
}
user, status, resolveErr := a.resolveUser(tokens.IDTokenClaims, info)
if resolveErr != nil {
ctx.AbortWithError(status, resolveErr)
return
}
client, err := a.createClient(session.ClientName, user.ID)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
ctx.JSON(http.StatusOK, &model.OIDCExternalTokenResponse{
Token: client.Token,
User: &model.UserExternal{ID: user.ID, Name: user.Name, Admin: user.Admin},
})
}
View on GitHub (pinned to 14bfc25627)
Solutions
- Check the wrapped cause for the provider's HTTP status
- Verify OIDC discovery (issuer/.well-known/openid-configuration) exposes a valid userinfo_endpoint
- Ensure network/TLS access from the server to the IdP
- Retry after confirming the access token is valid and unexpired
Defensive patterns
Strategy: retry
Validate before calling
// check discovery exposes userinfo
const disco = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());
if (!disco.userinfo_endpoint) throw new Error('provider has no userinfo endpoint'); Try / catch
try {
info = await userinfo(accessToken, tokenType, subject);
} catch (err) {
if (isTransientNetwork(err)) return retryWithBackoff();
if (isUnauthorized(err)) return startNewLoginFlow();
throw err;
} Prevention
- Monitor connectivity/TLS to the IdP from the server
- Validate the discovery document at startup
- Retry transient network failures with backoff
- Confirm the access token is valid for the userinfo endpoint
When it happens
Trigger: Access token rejected/expired at the userinfo endpoint, provider userinfo URL misconfigured or unreachable, network failure, or the provider returning an error status for the given subject/token type.
Common situations: Provider outage or internal network/DNS issues; token type mismatch (e.g. opaque vs JWT); discovery document missing userinfo_endpoint; TLS problems to the IdP.
Related errors
- token exchange failed: %w
- invalid client name
- unknown or expired state
- issuer claim was empty
- subject claim was empty
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/3d33197332601185.
Report an issue: GitHub.