Skip to content

fix(deployment): surface GitHub approval failures instead of a generic 500 - #1198

Open
krusche wants to merge 1 commit into
stagingfrom
fix/surface-github-approval-errors
Open

fix(deployment): surface GitHub approval failures instead of a generic 500#1198
krusche wants to merge 1 commit into
stagingfrom
fix/surface-github-approval-errors

Conversation

@krusche

@krusche krusche commented Jul 19, 2026

Copy link
Copy Markdown
Member

Motivation

Clicking Approve/Decline on a pending deployment showed a red toast reading "Error: An internal server error occurred" — with no hint of what actually went wrong. On prod the real cause was a GitHub HTTP 401 (the deployer's Keycloak-brokered GitHub token had expired), which the service correctly turns into a 502 "GitHub rejected the request: … HTTP 401".

That message never reached the user: GlobalExceptionHandler's generic @ExceptionHandler({Exception.class, IOException.class}) catches ResponseStatusException too (advice handlers run before Spring's ResponseStatusExceptionResolver), so every ResponseStatusException in the app was being flattened to a flat 500 "internal server error", discarding both the intended status and the message.

Prod log for the reported failure:

GitHubService: GitHub pending-deployment approved failed for run …: HTTP 401
DeploymentReviewActionService: In-app approve by @krusche for deployment 9436 failed at GitHub: HTTP 401
GlobalExceptionHandler: An internal server error occurred
  → ResponseStatusException: 502 BAD_GATEWAY "GitHub rejected the request: … HTTP 401"

Changes

  • GlobalExceptionHandler: add a dedicated @ExceptionHandler(ResponseStatusException.class) that preserves the intended status + reason. These are deliberate, handled outcomes, so they're kept out of Sentry (5xx logged at WARN, 4xx at DEBUG). This fixes the misleading error for all endpoints that throw ResponseStatusException, not just approvals.
  • GitHubReviewException (new, extends IOException): carries GitHub's HTTP status. The review path uses it to detect a 401 and return an actionable reason — "your GitHub authorization has expired. Please sign out of Helios and sign in again, then retry." — while still returning 502 to the client (returning 401 would make the client treat the user's Helios session as expired and bounce them to login).

Scope / follow-up

This makes the failure legible; it does not stop the 401s. The underlying cause — Keycloak stores the GitHub token at login and never refreshes it, and GitHub App user tokens expire after 8h, so auto- and in-app approval both fail once the token is stale — is being addressed separately (Keycloak refresh-token integration).

Testing

  • DeploymentApprovalControllerErrorHandlingTest (new, @WebMvcTest): a service-thrown 502 reaches the client as 502 with its reason intact; an unexpected exception still falls back to 500.
  • DeploymentReviewActionServiceTest (new case): a GitHub 401 yields 502 with the actionable "expired / sign in again" message and still records a FAILED_AT_GITHUB audit row.
  • Existing GitHubServiceTest failure case unchanged (GitHubReviewException is an IOException with the same message).

🤖 Generated with Claude Code

…c 500

When GitHub rejected an in-app deployment approval/decline, the service threw
a ResponseStatusException(502, "GitHub rejected the request: ...") — but the
GlobalExceptionHandler's generic Exception handler caught it first (advice
handlers run before Spring's ResponseStatusExceptionResolver) and flattened it
to a 500 "internal server error", discarding the status and message. Every
ResponseStatusException across the app was affected.

- Add a dedicated @ExceptionHandler(ResponseStatusException.class) that
  preserves the intended status + reason (and keeps these handled outcomes out
  of Sentry; 5xx logged at WARN, 4xx at DEBUG).
- Introduce GitHubReviewException (extends IOException) carrying GitHub's HTTP
  status so the review path can react to a 401 (expired brokered token) with an
  actionable message telling the reviewer to sign out and back in, while still
  returning 502 to the client (not 401, which would bounce them to login).

Root cause of the 401s themselves (expired Keycloak-brokered GitHub token, not
refreshed) is tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@krusche
krusche requested a review from a team as a code owner July 19, 2026 12:30
Copilot AI review requested due to automatic review settings July 19, 2026 12:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codacy-production

codacy-production Bot commented Jul 19, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0 (≤ 20 complexity)

View in Codacy

🟢 Coverage 91.67% diff coverage · +0.28% coverage variation

Metric Results
Coverage variation +0.28% coverage variation (-1.00%)
Diff coverage 91.67% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (8afae1a) Report Missing Report Missing Report Missing
Head commit (a38d148) 15993 (+104) 8626 (+101) 53.94% (+0.28%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#1198) 24 22 91.67%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@krusche This cleanly preserves intentional response statuses and gives expired GitHub authorization failures an actionable message while keeping the client-facing status at 502. Nice focused tests around both the controller response and audit-row behavior; approving as-is.

@github-actions

Copy link
Copy Markdown

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

@github-actions github-actions Bot added the stale label Jul 27, 2026
@github-actions github-actions Bot closed this Aug 11, 2026
@krusche krusche reopened this Aug 13, 2026
@krusche

krusche commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

we should still work on this and get it fixed: not stale

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants