Skip to content

fix: show why a write or load failed - #6016

Merged
norman-abramovitz merged 16 commits into
cloudfoundry:developfrom
nabramovitz:norm/fix/write-error-message
Oct 15, 2026
Merged

norman-abramovitz merged 16 commits into
cloudfoundry:developfrom
nabramovitz:norm/fix/write-error-message

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

A failed write told the user different things depending on how Cloud Foundry answered. When the job failed inside the fast-path window, the banner showed the error title; after polling, it showed the detail; and many other write and load paths showed [object Object], undefined (502) or Http failure response for … instead of the reason the server gave. This is the first group of #6002 (step 1 of its plan) plus the read paths found on the way.

One message for a failed write. A failed write now reads as Stratos's lead-in followed by the broker's or Cloud Foundry's own words (the detail, else the title), whichever path the job took. Any HTML page the error quoted is reduced to its text, as before. The CF error code and title are logged to the browser console (console.warn), so they are there for whoever needs them without crowding the banner.

A single helper for HTTP error text. extractHttpErrorMessage and stripMarkup now live in @stratosui/store, and every caller imports them from there; the local copies are gone. The paths that showed no reason now use it:

Where Showed
Creating or editing an organization, quota or space quota [object Object]
Deleting a service instance from a list, unbinding from its summary page [object Object]
Binding a route service, creating or deleting a service key undefined (502)
Attaching a newly created route to an application Http failure response for …
Detaching a service instance's bindings unknown error
Rolling back an application the error title only, or Rollback failed
Any wizard step whose submit failed with an HTTP error [object Object]
A detail page's error banner and header actions, adding an API key [object Object]
Failed loads of an organization, space, endpoint, application or CF info [object Object]

A rejected operation is not an endpoint fault. A job that failed inside the fast-path window was answered as 502, so the interceptor also raised "Cloud Foundry endpoint '…' Request failed." The backend now answers it through one helper, stratosjobs.WriteFailed: 422 with the same {state, errors} body and X-Stratos-Error-Reason: operation_failed (the header constant moved into stratosjobs so later upstreams classify the same way). RunFastPath's doc already promised a 4xx here; 22 handlers had drifted to 502. A transport failure never marks a job FAILED, so real endpoint faults keep unreachable and auth_expired. The frontend raises its endpoint banner only for those two reasons or an unclassified 502. Three contract goldens were regenerated (502 → 422).

Found during the live runs:

  • The schema form's Form/JSON toggle was bound with ngModel inside the form, logging NG01354. It is now plain radios with a per-instance name and ids (the wizard can show two schema forms).
  • Error snackbars were hard to read: the blue action was 1.02:1 on the red, and even the white text was 3.76:1 (light) and 2.77:1 (dark). They now use danger-700 (white 4.98:1) with a white underlined action and close button.

Tried: make check gate passes (lint, 3827 unit tests, Go tests, production build warnings). Live against a CF with overview-broker, both the fast path (sync + servererror) and the polled path (async + failasync) show one banner with the broker's message and log the CF code; the fast-path POST answers 422 and no endpoint banner appears; NG01354 is gone; the snackbar is readable.

Refs #6002.

Changelog fragment: changelog.d/0008-write-error-messages.md.

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.

The same broker failure read differently depending on how CF answered.
Inside the fast-path window the message was the broker's detail; after
a polled job it was CF's title in front of it ("CF-UnableToPerform.
..."), and only the polled path removed the HTML page CF quotes from a
broker's response.

A failed job's message is now the detail, markup stripped, or CF's
title when there is no detail; extractHttpErrorMessage reads the
fast-path body the same way, through the same stripMarkup. Callers add
their own lead-in. The three expected failures from cloudfoundry#6005 now pass.

Refs cloudfoundry#6002
The failure message now shows only the broker's or CF's explanation, so
the CF error code and title it leaves out are logged to the browser
console instead. writeWithJob does it, as the one place both the
fast-path HTTP error and the polled FAILED job pass through, however
each caller formats the message.

Refs cloudfoundry#6002
The create-service-instance step and CsiModeService each carried a
private copy of extractHttpErrorMessage. Use the shared one, so the
markup stripping and the fallbacks live in one place.
HttpErrorResponse is not an Error, so a failure that came back on the
fast path lost its reason wherever the message was built from
err.message or String(err): deleting a service instance from a list
and unbinding on its summary page showed "[object Object]", binding a
route service and creating or deleting a service key "undefined (502)",
detaching bindings "unknown error", and the orphan-route message
Angular's "Http failure response for ...". A rollback showed CF's title
from a failed job and "Rollback failed" otherwise.

All of them now use extractHttpErrorMessage. The service key
credentials read keeps httpErrorResponseToSafeString: it is not a write
and names the HTTP status on purpose.

Refs cloudfoundry#6002
Creating or editing an organization, quota or space quota built its
failure from err.message or String(err). The HTTP error is not an
Error, so CF's reason (a duplicate name, say) showed as "[object
Object]". Use extractHttpErrorMessage.
extractHttpErrorMessage and stripMarkup lived in cloud-foundry, so core
and kubernetes, which sit below it, could not use them and built their
messages from err.message or String(err) instead. Move both to
@stratosui/store beside httpErrorResponseToSafeString and import them
from there everywhere. A thrown string now keeps its text instead of
becoming "unknown error". No other behaviour change.
The stepper, the detail page's error banner and header actions, and the
add API key dialog built their message from err.message or String(err).
An HttpErrorResponse is not an Error, so a failure there read "[object
Object]". The stepper is the one most users meet: any wizard step whose
submit rejects with the HTTP error showed it. Use
extractHttpErrorMessage.
The org, space, CF info and endpoint data services, app detail and the
relation fetcher recorded a failed fetch as err.message or String(err),
so an HTTP error showed as "[object Object]" in their error states, and
the create-service-instance setup logged it the same way. Use
extractHttpErrorMessage. The relation post-processor and the kube
envelope catch only synchronous throws and keep their own handling.
A write whose CF job reached FAILED inside the fast-path window was
answered 502, so the frontend read a broker rejecting a provision as
the endpoint failing and showed "Cloud Foundry endpoint ... Request
failed" next to the real error. The endpoint had answered; the
operation it ran failed. RunFastPath's own contract already said 4xx.

The 22 handlers that wrote the envelope now call
stratosjobs.WriteFailed: 422, the same {state, errors} body, and a new
X-Stratos-Error-Reason value, operation_failed. The header constant
moves to stratosjobs so the CF plugin and any later upstream (BOSH,
CredHub, UAA) classify failures the same way. A transport failure
never marks a job FAILED (the translator keeps it PROCESSING), so
endpoint faults keep their own reasons.

Refs cloudfoundry#6002
The CF API interceptor raised its endpoint banner for any reason
header. Limit it to the reasons that name an endpoint fault
(auth_expired, unreachable) and to an unclassified 502; other reasons,
such as operation_failed, are left to the component that made the call.

Refs cloudfoundry#6002
The Form/JSON radios bound the view choice with ngModel. Inside the
create-service-instance step's form group, each tried to register as a
control and Angular warned NG01354 twice. The choice is view state, so
the radios now bind checked and set the signal on change, without
FormsModule. Each schema form gets its own radio group name and ids:
the wizard can show two of them (instance and binding parameters).
On the red error snackbar the primary-blue action measured 1.02:1
against the background and the grey close 1.48:1; even the white
message was 3.76:1 in light mode and 2.77:1 in dark, under the WCAG AA
4.5:1 for text. Error snackbars now use danger-700, where white is
4.98:1, with a white underlined action and a white close. Other
snackbars are unchanged.
Comment thread src/frontend/packages/store/src/jetstream.ts Fixed

@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.

LGTM

CodeQL flagged stripMarkup's tag pattern as a polynomial regular
expression on uncontrolled data. With only '>' excluded, a run of '<'
and no '>' (the text is the server's error body) backtracks
quadratically: 100k characters took 4.3 s. Excluding '<' from the tag
body as well keeps it linear, and a stray '<' in prose no longer
swallows the text up to the next tag.

@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.

LGTM - CodeQL fix

@norman-abramovitz
norman-abramovitz merged commit 1fd2932 into cloudfoundry:develop Oct 15, 2026
24 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.

3 participants