Skip to content

fix: serve error page without login and show a clear message for stale edits - #987

Merged
Goosetaf merged 2 commits into
mainfrom
fix/error-handling
Oct 23, 2026
Merged

Goosetaf merged 2 commits into
mainfrom
fix/error-handling

Conversation

@Portals

@Portals Portals commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Closes #951
Closes #907

Root causes

#951 – /error required authentication

  • Spring Security authorizes the container's ERROR dispatch. The web chain's anyRequest().authenticated() rejected the forward to /error for unauthenticated requests, so any error (e.g. a 400 from /oauth2/authorize?client_id=bogus…) became a 302 to /login and a 200 after redirects.
  • GammaErrorController never set the response status, so a direct GET /error (used by the existing redirect:/error) returned 200 with the 500 page.
  • pages/error.html hard-coded "500 - Internal Server Error" for every status.
  • common/header.html / header-without-nav.html evaluated #authentication.getDetails().gammaUser() for any authenticated principal. For errors raised in the API-key chain or the OAuth2 resource-server chain the principal is not a UserAuthentication, so rendering the error page itself threw.

#907 – stale edits produced a generic 500

  • MutableEntity.increaseVersion threw a JPA-adapter-level StaleDomainObjectException on a version mismatch and nothing handled it.

Fixes

  • SecurityFiltersConfig (web chain): permit DispatcherType.ERROR and GET /error. The other chains are unchanged; normal routes still require login.
  • GammaErrorController: @RequestMapping for all methods; always sets the status (unknown codes → 500); HTML handler for clients that accept text/html (browsers, htmx), JSON (ApiErrorBody) for everything else, mirroring Spring Boot's BasicErrorController; passes statusCode/statusReason to the template. Exception details are still only shown to admins.
  • Header templates only render the "Hey, nick!"/logout block when the principal is a UserAuthentication.
  • StaleDomainObjectException moved to the domain (it.chalmers.gamma.app.common) and thrown directly by MutableEntity.increaseVersion, so no per-facade translation is needed (adapters already throw domain exceptions). ThymeleafAdvice returns 409 with a non-auto-dismissing alert in #alerts (htmx, HX-Retarget/HX-Reswap) or a full 409 - Conflict page; ApiExceptionAdvice returns 409 JSON.

Tests added

  • e2e/tests/ui/error-page-does-not-require-login.it.ts: anonymous bogus authorize request → 400 HTML/JSON without a login redirect; GET /error → 500; /users still redirects to /login; browser shows "400 - Bad Request".
  • e2e/tests/api/api-error-responses.it.ts: unknown /api/info/v1/... path with a valid API key → 404 JSON/HTML without redirect or template crash; OAuth2 bearer token + bogus authorize → 400 JSON/HTML, userinfo still 200.
  • e2e/tests/ui/groups-admin-edit-stale-group.it.ts: group edited in a second tab, stale save → 409 and the "modified by someone else" alert; direct non-htmx form submit → 409 page; reload shows the other tab's value.

CI note

The first CI run hit the Run E2E tests step's 20 minute timeout (62 of 65 tests had run, no test failed). Every test boots fresh containers at 13-20 s per test depending on the runner, and main already timed out once the same way with 61 tests (run 39836548292). This PR raises that step's timeout to 30 minutes; the job-level limit stays at 60.

Verification

  • ./gradlew check (spotless + compile) and e2e format:check, lint, typecheck pass.
  • The three new tests plus groups-admin-edit-group, session-logout-protects-routes, api-info-endpoint-requires-valid-api-key and oauth2-authentication-contract pass against an image built from this branch (some runs hit container start-up timeouts because the dev machine's disk filled up; each test passed in at least one run and no assertion failed).

🤖 Generated with Claude Code

…e edits

Fixes #951: Spring Security authorized the container's ERROR dispatch to
/error, so any error for an unauthenticated request (for example a 400 from
the OAuth2 authorization endpoint) was turned into a redirect to /login and
ended as a 200. The web chain now permits the ERROR dispatcher type and GET
/error. GammaErrorController always sets the HTTP status (a direct GET /error
no longer returns 200), handles all request methods, renders HTML for clients
that accept it and JSON otherwise, and shows the real status code and reason
on the error page. The header templates no longer crash when the principal is
an API key or JWT, which broke the error page for errors raised in the API and
OAuth2 chains.

Fixes #907: a version mismatch in MutableEntity.increaseVersion now throws the
domain exception it.chalmers.gamma.app.common.StaleDomainObjectException.
ThymeleafAdvice renders a 409 alert (htmx) or a 409 page (plain request)
telling the user the item was modified by someone else and should be reloaded;
ApiExceptionAdvice returns 409 JSON.

E2E tests cover the anonymous error path, API-key and OAuth2 error responses,
and a stale group edit from two tabs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Portals
Portals requested a review from Goosetaf as a code owner October 22, 2026 21:40
Every test boots fresh Postgres, Redis and Gamma containers, which takes
13-20 s per test depending on the runner. With 65 tests the step can exceed
the previous 20 minute limit on a slow runner (main already timed out once
with 61 tests). The job-level timeout stays at 60 minutes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@Goosetaf Goosetaf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Goosetaf
Goosetaf merged commit 95b975e into main Oct 23, 2026
2 checks passed
@Goosetaf
Goosetaf deleted the fix/error-handling branch October 23, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

500 Error page (/error) requires authentication Provide clear error message when entity is out of date

2 participants