Skip to content

fix(cf): classify native CF endpoint errors (unreachable vs auth) in banner - #5385

Merged
norman-abramovitz merged 3 commits into
cloudfoundry:developfrom
nabramovitz:fix/cf-endpoint-error-reason
May 28, 2026
Merged

fix(cf): classify native CF endpoint errors (unreachable vs auth) in banner#5385
norman-abramovitz merged 3 commits into
cloudfoundry:developfrom
nabramovitz:fix/cf-endpoint-error-reason

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

Summary

Native CF handler failures all collapsed to HTTP 502 with the real cause buried in the message, so Stratos showed "authentication expired. Reconnect to refresh." for every failure — including an endpoint that's simply down (where reconnecting can't help) or a plain CF error.

This classifies the failure and surfaces it accurately.

Backend

  • classifyCfError — type-safe classification of UAA token-refresh errors (api.ErrHTTPRequest.Status) and capi CF-call errors (capi sentinels + net/timeout) into two called-out reasons: unreachable (5xx/network) and auth_expired (401/invalid_grant). Everything else stays unclassified.
  • Surfaced on both error paths so they reach the frontend:
    • classifyNativeErrors middleware wraps the native route group and shapes raw handler errors (newCapiClient now returns its raw refresh/client error instead of a pre-wrapped 502; 29 read-handler capi-call sites return the raw error).
    • handleCapiError (direct-write handlers) sets the same headers.
  • Headers: X-Stratos-Error-Reason + X-Stratos-Upstream-Status (the real CF/UAA code, since the Stratos status is a mapped 502). oauth_requests.RefreshOAuthToken now wraps with %w so the typed error stays unwrappable.

Frontend

  • cf-api-interceptor branches on the reason header:
    • auth_expired → reconnect banner + markStale
    • unreachable → "the endpoint is unreachable or down" + the upstream HTTP code, no reconnect/markStale
    • otherwise → neutral "Request failed"; normal CF errors (404/422) are left to the calling component
  • Two-line banner (endpoint name; then HTTP <code>: <what>), rendered via whitespace-pre-line — no <br>/innerHTML, so the user-controlled endpoint name can't inject markup. GUID dropped from the message.
  • EndpointAuthStateService.notifyOnce() dedups non-auth banners without marking an endpoint "needs reconnect".

Test plan

  • Backend: classifyCfError, nativeCFError, classifyNativeErrors, upstreamStatusOf unit tests; full cloudfoundry plugin suite green; go build ./... clean
  • Frontend: 12 cfApiInterceptor specs (auth_expired / unreachable / generic / header-driven / 404-no-banner) green
  • Browser-verified against a down endpoint (520-aws-lab): banner reads "Cloud Foundry endpoint '520-aws-lab' / HTTP 503: The endpoint is unreachable or down."
  • CI gate

…ders

Native CF handler failures all collapsed to HTTP 502 with the real
cause buried in the message, so the frontend showed "authentication
expired" for every failure — including an unreachable/down endpoint
or a plain CF error.

Add classifyCfError: type-safe classification of UAA token-refresh
errors (api.ErrHTTPRequest.Status) and capi CF-call errors (capi
sentinels + net/timeout) into two called-out reasons — unreachable
(5xx/network) and auth_expired (401/invalid_grant) — leaving normal
CF errors unclassified.

Surface it on two paths so both reach the frontend:
- classifyNativeErrors middleware wraps the native route group and
  shapes raw handler errors (newCapiClient now returns its raw
  refresh/client error instead of a pre-wrapped 502).
- handleCapiError (direct-write handlers) sets the same headers.

Both set X-Stratos-Error-Reason and X-Stratos-Upstream-Status (the
real CF/UAA code, since the Stratos status is a mapped 502). Body
uses a '{'-led JSON discriminator. oauth_requests RefreshOAuthToken
now wraps with %w so api.ErrHTTPRequest stays unwrappable.
Replace `echo.NewHTTPError(502, err.Error())` with the raw capi
error at 29 CF-call sites across the native read handlers. The
stringified form erased the error type, so the classifyNativeErrors
middleware could not tell an unreachable endpoint (CF 5xx) from a
normal CF error. Returning the raw error lets the middleware
classify it (and a CF 5xx mid-call now surfaces as unreachable).

String-literal and config errors ("no job id returned", "endpoint
not found", token-parse) are left as echo errors — they are not
capi errors and pass through the middleware unchanged.
The cf-api-interceptor showed "authentication expired. Reconnect"
for every native CF 502, even when the endpoint was simply down.

Branch on the X-Stratos-Error-Reason header:
- auth_expired → reconnect banner + markStale (token rejected)
- unreachable  → "the endpoint is unreachable or down" + the upstream
  HTTP code, no markStale/Reconnect (reconnect won't help a down API)
- otherwise    → neutral "Request failed" banner; normal CF errors
  (404/422, no reason, non-502) are left to the calling component

Two-line banner: endpoint name on line 1, "HTTP <code>: <what>" on
line 2 — rendered via the snackbar's whitespace-pre-line (no
<br>/innerHTML, so the user-controlled endpoint name can't inject
markup). The GUID is dropped from the message; the upstream code
comes from X-Stratos-Upstream-Status. EndpointAuthStateService gains
notifyOnce() to dedup non-auth banners without marking an endpoint
"needs reconnect".

@norman-abramovitz norman-abramovitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Saw the 503 error for the endpoint not reachable or down

@norman-abramovitz
norman-abramovitz merged commit aec49db into cloudfoundry:develop May 28, 2026
12 checks passed
@nabramovitz
nabramovitz deleted the fix/cf-endpoint-error-reason branch May 28, 2026 10:41
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.

2 participants