Skip to content

Keep raw handler failures out of problem documents and publish the cause's own detail over the bus - #21

Merged
ancongui merged 5 commits into
mainfrom
fix/cqrs-problem-detail
Oct 13, 2026
Merged

ancongui merged 5 commits into
mainfrom
fix/cqrs-problem-detail

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

Stops problem documents from leaking a command handler's raw failure, and makes the bus publish exactly what a controller would.

Security

A command or query whose handler threw a non-Firefly throwable produced a 500. Its detail contained the command's PHP class and the raw cause message, even with disclosure off (app.debug=false, firefly.web.problem.disclose=false). Reproduced through the real HTTP pipeline:

Processing command [App\…\FailingCommand] failed: SQLSTATE[HY000]: … (SQL: select * from users where token = tok_live_5ecret)

The same throwable thrown from a controller is rendered as the opaque sentence (An unexpected error occurred. It has been logged; quote reference <id>…). Bus failures now get that sentence too.

OrchestrationController::guard() published any FireflyException's raw message as error; it now publishes the authored detail or a fixed sentence. Two other wrappers that quoted raw throwables (EventMapper, orchestration ConditionEvaluator) now withhold their detail.

Change

FireflyException::detail(): ?string is the client-facing sentence, distinct from getMessage(), which stays the log message. It returns getMessage() by default. withDetail() sets it, and null means "nothing authored for a client".

  • ErrorResponse::fromException() and the HTML error page publish detail().
  • The CQRS wrappers carry their cause's detail. The Processing command […] failed: prefix stays in logs only.
  • When detail() is null, ProblemMapper uses the opaque sentence, or the root cause's message if disclosure is on. It keeps the wrapper's code, status, category, title and extensions.

A refused or failed command over the bus now produces the same detail as the same exception thrown from a controller.

Verification

  • composer check: exit 0, 6812 passed. Includes BusFailureProblemDocumentTest (13 cases, all failing before the fix) and BusRefusalProblemDocumentTest, which now requires identical documents including detail.
  • Integration lanes: 386 passed.
  • bash scripts/build-docs.sh: exit 0. Book chapters 4, 7 and 10 (EN + ES) and the error-handling, cqrs and security guides are updated.

Andres Contreras added 5 commits October 13, 2026 16:49
… message

ErrorResponse::fromException() now publishes detail(), which is getMessage()
by default. withDetail(?string) replaces it on the instance without touching
the message the log reads; null means the exception holds no sentence written
for a client.
…dled throwable

A FireflyException whose detail() is null keeps its code, status, category,
severity, title and extension members, but its sentence is the opaque one
(with the reference) unless the deployment discloses, and then the message of
the throwable underneath every such wrapper. The HTML page's authored detail
reads detail() instead of getMessage().
…s as detail

Command/QueryProcessingException messages read "Processing command [<class>]
failed: <cause message>", and that message was the problem document's detail:
a handler's QueryException put its SQL and bindings on the wire with
disclosure off, and an authored refusal carried the class name in front of
its sentence. The wrappers now publish the cause's own detail(), or null when
the cause is not a FireflyException, so the document matches the one the same
throwable gets from a controller. The message keeps the prefix for the log.
…e wire

EventMapper's EventSerializationException for an event constructor that threw
and ConditionEvaluator's ORCHESTRATION_CONDITION_FAILED quote an arbitrary
throwable; both now withhold their detail. The orchestration REST guard
publishes detail() and a fixed sentence when it is withheld, instead of any
refusal's message.
The wire detail is the cause's own sentence, a non-Firefly cause is opaque
unless disclosed, and the "Processing command [...] failed:" prefix stays on
the wrapper's message for the log. Adds the CHANGELOG Security and Changed
entries.
@ancongui
ancongui merged commit 7a8f5cc into main Oct 13, 2026
12 checks passed
@ancongui ancongui mentioned this pull request Oct 13, 2026
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