gotify/server · error

database error

Error message

database error: %w

What it means

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'.

Solutions

  1. Check DB connectivity and the wrapped cause in logs
  2. Run pending database migrations
  3. Verify the schema used by GetUserByOIDC (index/columns on oidc ID)
  4. Restart/reconnect the database and retry login
Defensive patterns

Strategy: try-catch

Validate before calling

// health-check the DB before the login flow
const healthy = await db.ping().then(() => true).catch(() => false);
if (!healthy) throw new Error('database unavailable');

Try / catch

try {
  user = await db.getUserByOIDC(oidcID);
} catch (err) {
  if (isConnectionError(err)) return retryWithBackoff(err);
  if (isMissingTable(err)) return runMigrations();
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: Database not migrated (missing oidc column/table); DB temporarily unreachable; connection pool exhausted; migration version mismatch after upgrade.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/be5f4c825efd99f1. Report an issue: GitHub.

Appendix: source

Thrown at api/oidc.go:445

	issuer := idToken.GetIssuer()
	if issuer == "" {
		return nil, http.StatusInternalServerError, errors.New("issuer claim was empty")
	}
	if _, err := url.Parse(issuer); err != nil {
		return nil, http.StatusInternalServerError, fmt.Errorf("issuer url %q is not a valid url: %w", issuer, err)
	}
	if strings.Contains(issuer, "#") {
		return nil, http.StatusInternalServerError, fmt.Errorf("issuer url %q may not contain a fragment", issuer)
	}
	subject := info.GetSubject()
	if subject == "" {
		return nil, http.StatusInternalServerError, errors.New("subject claim was empty")
	}
	oidcID := issuer + "#" + subject

	user, err := a.DB.GetUserByOIDC(oidcID)
	if err != nil {
		return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
	}

	hasAdminGroup, status, err := a.resolvePermission(idToken.Claims, info.Claims)
	if err != nil {
		log.Err(err).Str("oidc_id", oidcID).Interface("idTokenClaims", idToken.Claims).Interface("userinfoClaims", info.Claims).Msg("OIDC: resolve permission")
		return nil, status, err
	}

	if user != nil {
		if len(a.GroupsAdmin) > 0 && user.Admin != hasAdminGroup {
			user.Admin = hasAdminGroup
			if err := a.DB.UpdateUser(user); err != nil {
				return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
			}
			log.Warn().Str("oidc_id", oidcID).Str("username", user.Name).Bool("admin", user.Admin).Msg("OIDC change permission")
		}
		return user, 0, nil
	}

View on GitHub (pinned to 14bfc25627)