Skip to content

fix: a refusal reaches the practitioner in the API's own words - #126

Merged
ShocOne merged 1 commit into
mainfrom
fix/a-refusal-reaches-the-practitioner-in-the-apis-words
Aug 26, 2026
Merged

fix: a refusal reaches the practitioner in the API's own words#126
ShocOne merged 1 commit into
mainfrom
fix/a-refusal-reaches-the-practitioner-in-the-apis-words

Conversation

@ShocOne

@ShocOne ShocOne commented Aug 26, 2026

Copy link
Copy Markdown
Member

The defect

kiota deserializes an error body into the properties the document declared, then throws them away:

func (m *Credentials_API_Error) Error() (string) {
    return m.ApiError.Error()          // the embedded parent
}
func (e *ApiError) Error() string {
    if len(e.Message) > 0 { return fmt.Sprint(e.Message) }
    return "error status code received from the API"
}

Nothing populates Message from the detail, title or status the generated type just deserialized. So that literal — the SDK's fallback for an unset field — was the detail of every refusal a generated provider reported. The words were in memory on the error's own getters, and the extractor read only Error().

Why the status is not enough on its own

Against the ThousandEyes pilot, a single HTTP 400 stood for six unrelated causes:

entity what the API actually said
account_group An Account Group with that name already exists.
role Error in field roleName : must not be null.
stream URL '…' is not reachable due to: unknown host
operations_webhook missing type id property 'type'
tests_ftp_server Error in field agentSet : No agents selected.
endpoint_label Request validation failed

A collision that should have been a 409, a missing field, a semantic reachability check, an absent discriminator, an empty relation, and one opaque even in prose. The status separates none of them; the message separates five of six.

The fix

The kiota extractor reads what the API sent: the problem document's summary and explanation joined when both are present and distinct, then the single property that carried a sentence, then the listed complaints among the properties the document did not declare (GetAdditionalData, which kiota keeps).

The key vocabulary is the audit's own refusalMessage (internal/audit/run/adjust.go) — listed complaints first, then detail, message, error_description, errorMessage, error, title — so the audit and the provider read a refusal the same way instead of inventing a second spelling.

This brings the dialect level with its sibling: extract_openapigenerator.go.tmpl already answered with the raw response body. kiota was the odd one out.

Measured, against a generated tree

thousandeyes_role, before:

Error: Create failed (HTTP 400)
error status code received from the API

after:

Error: Create failed (HTTP 400)
There were some errors in your request, please correct them before trying
again. Error in field roleName : must not be null.

That message names the fix. The old one named nothing.

Contract

Provider-core change, so it is a contract change under semver — every generated provider's error detail changes shape.

Verification

make check (90.7% total, emit 91.4%) and golangci-lint run (0 issues). The pilot regenerates, postcheck passes, and the message above is from the live acceptance run, not from grepping this repo.

Related

My #122 dropped error-response content on 27 DELETE operations to fix the Accept header, which set their errorMapping to nil — those deletes now yield an untyped ApiError and gain nothing from this change.

The cost is narrower than it first looks. A 404 on delete never reaches the message at all: HandleDeleteError returns early on it, because the object being gone is what the delete wanted. Retryable statuses are outwaited by IsRetryableDelete. What is left is a delete refused with a status that is neither — a 403, or a genuine 400/409 — where the practitioner now sees kiota's no error factory is registered for this code: N instead of the API's sentence.

Observed exactly once on the pilot, on account_group's delete, and that case has since resolved on its own: the objects holding the name were removed and the delete now answers 204. So the residual cost is real but currently unexercised. Worth revisiting separately if a delete refusal ever needs to explain itself.

kiota deserializes an error body into the properties the document declared and
never copies any of them onto the embedded ApiError, whose Error() then
answers one constant however much the API said. Every refusal a generated
provider reported read "error status code received from the API", which names
no field, no value and no condition.

The status alone does not separate them. Against one pilot a single 400 stood
for a name collision, a missing field, an unreachable URL, an absent
discriminator, an empty relation and an opaque validation failure; the message
separates five of the six.

The kiota extractor now reads what the API sent: the problem document's
summary and explanation joined when both are present and distinct, then the
single property that carried a sentence, then the listed complaints among the
properties the document did not declare. The vocabulary is the audit's own
refusal reader, so both stages read a refusal the same way rather than
inventing a second spelling for it.

This brings the dialect level with the other one: the openapi-generator
extractor already answered with the raw response body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShocOne
ShocOne merged commit 1830b49 into main Aug 26, 2026
2 checks passed
@ShocOne
ShocOne deleted the fix/a-refusal-reaches-the-practitioner-in-the-apis-words branch August 26, 2026 15:07
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