gotify/server · error

client not found

Error message

client not found

What it means

handleElevationCallback (api/oidc.go:264) returns HTTP 404 'client not found' when GetClientByID returns no client for elevate.ClientID, or the client exists but belongs to a different user (client.UserID != user.ID). This is a deliberate ownership/authorization guard, not a database failure.

Source

Thrown at api/oidc.go:264

		}
		auth.SetCookie(w, client.Token, auth.CookieMaxAge, a.SecureCookie)
		// A reverse proxy may have already stripped a url prefix from the URL
		// without us knowing, we have to make a relative redirect.
		// We cannot use http.Redirect as this normalizes the Path with r.URL.
		w.Header().Set("Location", "../../")
		w.WriteHeader(http.StatusTemporaryRedirect)
	}
	return gin.WrapF(rp.CodeExchangeHandler(rp.UserinfoCallback(callback), a.Provider))
}

func (a *OIDCAPI) handleElevationCallback(w http.ResponseWriter, elevate *pendingElevation, user *model.User) {
	client, err := a.DB.GetClientByID(elevate.ClientID)
	if err != nil {
		http.Error(w, fmt.Sprintf("database error: %v", err), http.StatusInternalServerError)
		return
	}
	if client == nil || client.UserID != user.ID {
		http.Error(w, "client not found", http.StatusNotFound)
		return
	}
	elevatedUntil := time.Now().Add(time.Duration(elevate.DurationSeconds) * time.Second)
	if err := a.DB.UpdateClientElevatedUntil(client.ID, &elevatedUntil); err != nil {
		http.Error(w, fmt.Sprintf("failed to elevate session: %v", err), http.StatusInternalServerError)
		return
	}

	// The UI rechecks the authentication when the tab is closed.
	w.WriteHeader(http.StatusOK)
	w.Header().Add("content-type", "text/html")
	io.WriteString(w, `<!DOCTYPE html>
<html lang="en">
<head>
  <title>Gotify Session Elevation</title>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
</head>

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Verify the client ID still exists and is owned by the logged-in user, then restart the elevation flow from the UI
  2. If the client was deleted, create it again and elevate the new client
  3. Do not reuse or share elevation URLs; each is bound to one user and one client
  4. Check that the same database is being used across restarts/replicas
  5. Confirm the elevation was initiated for the account you actually authenticated with at the provider
Defensive patterns

Strategy: validation

Validate before calling

// Before starting the elevation flow, confirm the client exists and is yours
client, err := db.GetClientByID(clientID)
if err != nil {
    return err // DB problem, different failure mode
}
if client == nil || client.UserID != currentUser.ID {
    // do not start the elevation; it is guaranteed to 404 at the callback
    return errors.New("client missing or not owned by current user")
}

Try / catch

const res = await fetch(elevateCallbackURL);
if (res.status === 404 && (await res.text()).includes('client not found')) {
  // client was deleted or belongs to another user: refresh client list,
  // re-create if needed, then start a NEW elevation flow
  await refreshClients();
  return startElevation();
}

Prevention

When it happens

Trigger: During the OIDC elevation callback: (1) the client was deleted between starting the elevation (ElevateHandler) and completing the provider round-trip; (2) the elevation state references a client owned by another user, e.g. after the state was crafted/replayed for a different account; (3) a stale elevation request referencing an ID from a different database/environment; (4) wrong ClientID stored in the pending elevation.

Common situations: User deletes the client (or an admin purges it) while the elevation tab is open at the provider; replaying an old elevation URL after re-authenticating as a different user; pointing gotify at a restored/migrated database missing that client row.

Related errors


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