Skip to content

fix(errorpages): answer API clients with JSON instead of an HTML page - #60

Merged
ashishmax31 merged 1 commit into
mainfrom
fix/error-page-content-negotiation
Aug 2, 2026
Merged

fix(errorpages): answer API clients with JSON instead of an HTML page#60
ashishmax31 merged 1 commit into
mainfrom
fix/error-page-content-negotiation

Conversation

@ashishmax31

Copy link
Copy Markdown
Contributor

Problem

The branded Traefik error pages from #52 are wired at the entrypoint level (charts/stackdome-agent/values.yaml), so the middleware wraps every router on web and websecure. That includes the router for the Stackdome API server, which the hub installer exposes as a StackResource (install/manifests/api-server-resource-cr.yaml, exposeToPublic: true).

Result: any 5xx from /api/v1/... reached the dashboard as text/html. axios could not parse it, so the UI lost both the error message and any structured detail.

Fix

Traefik copies the original request's headers onto the request it makes to the error-page service (custom_errors.go: utils.CopyHeaders(pageReq.Header, req.Header)), so Accept still identifies the real caller.

caller Accept response
browser navigation text/html,... branded page, unchanged
axios / API client application/json, text/plain, */* {"type":"Error","reason":"Internal Server Error"}
no Accept header same envelope

The status code is untouched in every case, and the envelope matches the API's own Error shape (pkg/api/error_types.go in the api-server repo), so getErrorMessage() in the dashboard reads .reason with no frontend change.

The match is on an explicit text/html rather than */*: axios sends */* as its last alternative, so treating that as "browser" would reintroduce the bug for every API client. There is a test entry pinning exactly that.

What this does not do

It cannot return the backend's own error text. Traefik's errors middleware discards the caught response body before the error-page service is ever reached:

// We don't care about the contents of the response, since we want to serve the ones from the error page, so we just drop them.
codeCatcher.Write

Recovering the real message means the middleware must not wrap API routes at all, which needs two routers per host split on Accept (a Traefik IngressRoute matching HeaderRegexp("Accept", "text/html") at higher priority, with the plain Ingress as passthrough). That makes the Traefik CRDs a runtime dependency of the operator and duplicates TLS config across two objects — deliberately left out of this PR.

Note 4xx never reaches this middleware, so validation, auth and conflict errors have always arrived intact. This only affects 5xx.

Test plan

  • go build ./... — OK
  • go test ./internal/... ./pkg/... ./cmd/... — 16/16 packages pass
  • gofmt, go vet clean
  • New DescribeTable in internal/errorpages/server_test.go covers browser / axios / no-header; existing page specs now assert Accept: text/html explicitly

Deploy note: the installer pins defaultChartVersion, so a chart version bump is needed for a VPS install to pick this up.

The Traefik error-page middleware added in #52 is applied at the entrypoint
level, so it wraps every router on both entrypoints — including the router
for the Stackdome API server that the hub installer exposes as a
StackResource. Any 5xx from the API therefore reached the dashboard as an
HTML page, which axios cannot parse: the UI lost both the status and the
message.

Traefik copies the original request's headers onto the request it makes to
the error-page service, so Accept still identifies the real caller. Serve the
page to a browser navigation, and the API error envelope the dashboard
already parses to everything else. The status code is unchanged either way.

The match is on an explicit text/html rather than */*, because axios sends
"application/json, text/plain, */*" — treating */* as a browser would
reintroduce the bug for every API client.

Note this cannot recover the backend's own error text: Traefik's errors
middleware discards the caught response body before the error-page service is
reached. Carrying the real message through needs Accept-split routers, which
is a larger change.
@ashishmax31
ashishmax31 merged commit 2d8e1a1 into main Aug 2, 2026
2 checks passed
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.

1 participant