Files
coder/coderd/x/chatd/chaterror
Ethan 1b9745c311 fix: surface chat error diagnostics (#26367)
Closes CODAGT-223

## What's already on `main` (via #25803)

#25803 fixed how `detail` is *rendered* when present:
`ChatStatusCallout` shows `status.detail` in a monospace `<code>` block
for `kind === "generic"`, `AgentChatPage` reads
`error.response?.data?.detail` inline, and the auth message was
tightened.

It did not fix `detail` being absent in the first place.

## The gap

`chaterror.Classify` only populates `Detail` from
`*fantasy.ProviderError` (OpenAI-shaped JSON envelope). Every other
realistic failure shape produces blank `Detail`:
`context.DeadlineExceeded`, `Post "…": connection refused`, `stream
error: stream ID …; INTERNAL_ERROR`, `Post "https://api.openai.com/…":
400 invalid model: gpt-9000`, `fantasy.Error` from the stream decoder,
`xerrors.New("status 401 from upstream")`, HTTP/2 peer resets. Users
still see the dead-end alert: "Request failed / The chat request failed
unexpectedly." with no third line.

## The fix

A new `chaterror.FormatDiagnosticDetail` entry point shares
diagnostic-detail logic with `classify.go`: non-auth rule-table branches
now fall back to a bounded raw error string when structured detail is
absent, while auth-classified failures keep only structured provider
detail. Curated branches (canceled, interrupted, Responses-API,
stream-incomplete, chain-broken) are left alone. The `exp_chats.go` POST
catch-all uses the exported helper, so the backend consistently emits a
bounded diagnostic string instead of leaving `Detail` blank. Fallback
diagnostics redact URLs preserved in typed transport errors by stripping
userinfo, query strings, and fragments before display, which keeps
provider error text useful while reducing credential exposure from
standard request URL wrappers.

## Security

This change surfaces upstream error text in the chat UI, where it is
also persisted in `chats.last_error`, so it crosses a trust boundary.
Codex brought this up as an issue through reviews. Mindful of cases like
#20968, where a sensitive field leaked into agent logs, the design
deliberately narrows what can reach a user:

- Auth-classified failures keep only structured provider detail and
never fall back to the raw error string.
- Fallback diagnostics redact any URL preserved in a typed `*url.Error`
by removing userinfo, query strings, and fragments, so credentials in
standard transport URL wrappers do not leak.
- Request-side credentials are not exposed: providers authenticate via
headers, and `fantasy.ProviderError.Error()` does not print the URL or
request dump. Dumped response headers are stripped before parsing, and
detail is length-capped.

The remaining channels are structured provider detail (`error.message`
from the provider's response body), which is surfaced verbatim because
it is the useful diagnostic this PR exists to deliver, and
already-flattened fallback text where typed transport context has been
lost. A well-behaved provider returns a description of the failure here,
not a secret; OpenAI, for example, masks the middle of the submitted key
and returns only a short fragment alongside a docs link. For a real
secret to appear, the upstream API, or a proxy an admin points
`base_url` at, would have to echo a plaintext credential into its own
error body or flattened error prose. I judge that any secret leakage as
a result of this PR would require a misbehaving API or middleware, and
that the usefulness of real diagnostics outweighs that bounded risk.
2026-06-16 00:21:13 +10:00
..