{"record":{"id":"be5f4c825efd99f1","repo":"gotify/server","slug":"database-error-w","errorCode":null,"errorMessage":"database error: %w","messagePattern":"database error: %w","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":445,"sourceCode":"\tissuer := idToken.GetIssuer()\n\tif issuer == \"\" {\n\t\treturn nil, http.StatusInternalServerError, errors.New(\"issuer claim was empty\")\n\t}\n\tif _, err := url.Parse(issuer); err != nil {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"issuer url %q is not a valid url: %w\", issuer, err)\n\t}\n\tif strings.Contains(issuer, \"#\") {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"issuer url %q may not contain a fragment\", issuer)\n\t}\n\tsubject := info.GetSubject()\n\tif subject == \"\" {\n\t\treturn nil, http.StatusInternalServerError, errors.New(\"subject claim was empty\")\n\t}\n\toidcID := issuer + \"#\" + subject\n\n\tuser, err := a.DB.GetUserByOIDC(oidcID)\n\tif err != nil {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"database error: %w\", err)\n\t}\n\n\thasAdminGroup, status, err := a.resolvePermission(idToken.Claims, info.Claims)\n\tif err != nil {\n\t\tlog.Err(err).Str(\"oidc_id\", oidcID).Interface(\"idTokenClaims\", idToken.Claims).Interface(\"userinfoClaims\", info.Claims).Msg(\"OIDC: resolve permission\")\n\t\treturn nil, status, err\n\t}\n\n\tif user != nil {\n\t\tif len(a.GroupsAdmin) > 0 && user.Admin != hasAdminGroup {\n\t\t\tuser.Admin = hasAdminGroup\n\t\t\tif err := a.DB.UpdateUser(user); err != nil {\n\t\t\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"database error: %w\", err)\n\t\t\t}\n\t\t\tlog.Warn().Str(\"oidc_id\", oidcID).Str(\"username\", user.Name).Bool(\"admin\", user.Admin).Msg(\"OIDC change permission\")\n\t\t}\n\t\treturn user, 0, nil\n\t}","sourceCodeStart":427,"sourceCodeEnd":463,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L427-L463","documentation":"After validating the issuer and subject, resolveUser looks up an existing user by the composite oidcID via `a.DB.GetUserByOIDC`. Any error returned by the database layer is wrapped as 'database error: %w' and surfaced as 500, distinguishing persistence failures from 'user not found'.","triggerScenarios":"GetUserByOIDC returns a non-nil error during the external token login flow — e.g. the users table is missing/corrupted, the DB connection is down, or the query fails for schema reasons.","commonSituations":"Database not migrated (missing oidc column/table); DB temporarily unreachable; connection pool exhausted; migration version mismatch after upgrade.","solutions":["Check DB connectivity and the wrapped cause in logs","Run pending database migrations","Verify the schema used by GetUserByOIDC (index/columns on oidc ID)","Restart/reconnect the database and retry login"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// health-check the DB before the login flow\nconst healthy = await db.ping().then(() => true).catch(() => false);\nif (!healthy) throw new Error('database unavailable');","typeGuard":null,"tryCatchPattern":"try {\n  user = await db.getUserByOIDC(oidcID);\n} catch (err) {\n  if (isConnectionError(err)) return retryWithBackoff(err);\n  if (isMissingTable(err)) return runMigrations();\n  throw err;\n}","preventionTips":["Run migrations as part of deployment","Monitor DB connectivity and pool saturation","Distinguish not-found (nil user) from real DB errors in the repository layer"],"tags":["database","oidc","persistence","sql"],"backgroundTag":"database-query-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"}