{"record":{"id":"3d33197332601185","repo":"gotify/server","slug":"failed-to-get-user-info-w","errorCode":null,"errorMessage":"failed to get user info: %w","messagePattern":"failed to get user info: %w","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":392,"sourceCode":"\t\treturn\n\t}\n\tsession, ok := a.popPendingSession(req.State)\n\tif !ok {\n\t\tctx.AbortWithError(http.StatusBadRequest, errors.New(\"unknown or expired state\"))\n\t\treturn\n\t}\n\texchangeOpts := []rp.CodeExchangeOpt{\n\t\trp.CodeExchangeOpt(rp.WithURLParam(\"redirect_uri\", session.RedirectURI)),\n\t\trp.WithCodeVerifier(req.CodeVerifier),\n\t}\n\ttokens, err := rp.CodeExchange[*oidc.IDTokenClaims](ctx.Request.Context(), req.Code, a.Provider, exchangeOpts...)\n\tif err != nil {\n\t\tctx.AbortWithError(http.StatusUnauthorized, fmt.Errorf(\"token exchange failed: %w\", err))\n\t\treturn\n\t}\n\tinfo, err := rp.Userinfo[*oidc.UserInfo](ctx.Request.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), a.Provider)\n\tif err != nil {\n\t\tctx.AbortWithError(http.StatusInternalServerError, fmt.Errorf(\"failed to get user info: %w\", err))\n\t\treturn\n\t}\n\tuser, status, resolveErr := a.resolveUser(tokens.IDTokenClaims, info)\n\tif resolveErr != nil {\n\t\tctx.AbortWithError(status, resolveErr)\n\t\treturn\n\t}\n\tclient, err := a.createClient(session.ClientName, user.ID)\n\tif err != nil {\n\t\tctx.AbortWithError(http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\tctx.JSON(http.StatusOK, &model.OIDCExternalTokenResponse{\n\t\tToken: client.Token,\n\t\tUser:  &model.UserExternal{ID: user.ID, Name: user.Name, Admin: user.Admin},\n\t})\n}\n","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L374-L410","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// check discovery exposes userinfo\nconst disco = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());\nif (!disco.userinfo_endpoint) throw new Error('provider has no userinfo endpoint');","typeGuard":null,"tryCatchPattern":"try {\n  info = await userinfo(accessToken, tokenType, subject);\n} catch (err) {\n  if (isTransientNetwork(err)) return retryWithBackoff();\n  if (isUnauthorized(err)) return startNewLoginFlow();\n  throw err;\n}","preventionTips":["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"],"tags":["oidc","oauth2","http","network","userinfo"],"backgroundTag":"oidc-userinfo-fetch-failed","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}