Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions changelog/+adopted-codes-raise-their-own-class.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
A failure the server reports as `NODE_NOT_FOUND`, `BRANCH_NOT_FOUND`, or `SCHEMA_NOT_FOUND` now raises `NodeNotFoundError`, `BranchNotFoundError`, or `SchemaNotFoundError` respectively, built from the payload the server sent, instead of a generic `GraphQLError`. One class therefore covers a lookup miss however it arose, and telling one apart from any other GraphQL failure no longer means matching words in a message:

```python
try:
await client.delete(kind="NetworkDevice", id=device_id)
except NodeNotFoundError:
... # already gone
```

This applies where the envelope carries the payload fields that the code's class needs. A server predating the error catalogue, or one whose payload the SDK cannot read, still raises `GraphQLError` as it does today, so keep any existing fallback until you no longer talk to such a server.

Each of the three classes is caught by `except GraphQLError` exactly as the generic one is, so no clause stops catching what it catches today. A ladder that handles a specific class differently will now see server-reported failures arrive there as well as client-side ones; `exc.code is not None` distinguishes the two.
1 change: 1 addition & 0 deletions changelog/+api-token-no-relogin-retry.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A client authenticating with an API token no longer replays a request after a 401 that reports an expired token. Only a client configured with a username and password can obtain a new token, so on the request paths that retry automatically the retry sent the same rejected token a second time, doubling the cost of the failure. Streaming downloads never retried and are unaffected.
5 changes: 5 additions & 0 deletions changelog/+catalogued-error-messages.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
A failure the server's error catalogue describes now carries a message naming that code and the server's own words, in place of the query text. `GraphQLError` previously rendered as `An error occurred while executing the GraphQL Query <query>, <errors>` and now reads `UNIQUENESS_VIOLATION: Node of kind TestPerson already has name 'John'`; `AuthenticationError` reads `AUTHENTICATION_REQUIRED: <reason>`. On both transports the code names the first error only, which is the one that determines the exception raised; the complete list stays on `exc.errors`, and the query and variables stay on `exc.query` and `exc.variables`.

A failure the catalogue does **not** describe keeps today's message exactly, query text and full error list included, as does one of the lookup-miss classes raised without a server behind it. That covers a server predating the catalogue, an error carrying no `extensions`, and, since a current server codes every error it reports, anything the server coded `UNDEFINED_ERROR`. `exc.code` is readable in every case, so it is not the test for which message form you are holding: `code_names_the_failure(exc.code)`, importable from `infrahub_sdk.exceptions`, is.

In `infrahubctl`, a described failure is now reported by its code and message rather than prefixed with `Authentication failure:` or rendered as a bare error list. An undescribed one renders exactly as before, so a GraphQL validation error still shows the line and column it failed on. Code matching on any of these strings should branch on `exc.code` instead.
1 change: 1 addition & 0 deletions changelog/+ctl-error-markup-escaping.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`infrahubctl` no longer deletes bracketed text from an error message. A message such as `The requested branch was not found on the server [main]` was printed without the branch name, because rich read the brackets as a style tag. Everything the shared error handler prints is now escaped, including the traceback the fallback branch emits. Affects branch, schema, and node lookup misses, authentication failures, HTTP transport failures, and the fallback handler.
1 change: 1 addition & 0 deletions changelog/+ctl-graphql-error-output.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`infrahubctl` no longer exits non-zero with no output when a GraphQL failure arrives in a shape the SDK cannot read as an error envelope. `infrahubctl run`, `infrahubctl validate graphql-query`, and every command wrapped by the shared error handler now fall back to printing the exception's message, which keeps the server's payload verbatim.
5 changes: 5 additions & 0 deletions changelog/+error-catalogue-envelope.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Authentication failures now surface the server's error envelope. `AuthenticationError` and `GraphQLError` share a new `ApiError` base carrying `code`, `http_status`, `extensions`, and `errors`, so a caller can branch on the server's catalogue code instead of matching on message text.

A 401 or 403 whose body the SDK cannot read as an error envelope now raises `AuthenticationError` carrying the best reason available: the REST API's bare `detail` string where the body has one, and otherwise the plain status. Previously the same responses raised `JsonDecodeError` (or, from the object store and file handler, a raw `json.JSONDecodeError`) when the body was not JSON, and `TypeError` when the body carried an `errors` array whose entries had no `message`. A body that was JSON but carried no `errors` key raised `AuthenticationError` with its generic default message, dropping the status the server sent. Code that catches those types around `object_store`, `file_handler`, or a client request should catch `AuthenticationError` instead.

`from infrahub_sdk.exceptions import *` now yields exactly the exception classes. It previously also carried whatever the module imported for its own annotations, such as `Mapping` and `Any`. Every exception class keeps its name and its import path.
5 changes: 5 additions & 0 deletions changelog/+except-graphql-error-broadened.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
`NodeNotFoundError`, `BranchNotFoundError`, and `SchemaNotFoundError` now descend from `GraphQLError`, so that one class covers a lookup miss however it arose: reported by the server, decided by the SDK, or turned from a REST 404.

An `except GraphQLError` clause therefore also catches lookup misses that involved no GraphQL request at all. Code that relied on those escaping such a clause should catch the specific class ahead of it, as an ordered `except` ladder already must. Each class keeps its name, its constructor, and the message it produces when no server reported the failure, and `errors`, `query`, and `variables` are now readable on every one of them rather than missing on a client-side raise.

`NodeInvalidError` inherits the re-rooting but not the adopted code: it means a node of the wrong kind rather than a lookup miss, so `NodeInvalidError.CODE` is `None` where `NodeNotFoundError.CODE` is `NODE_NOT_FOUND`.
1 change: 1 addition & 0 deletions changelog/+file-handler-404-body.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A file download answered with HTTP 404 now raises `NodeNotFoundError` whatever the response body contains. Previously a body that was not a JSON object - an HTML error page from an intermediary, an empty body, or a JSON array - escaped as a raw `json.JSONDecodeError` or `AttributeError` instead.
1 change: 1 addition & 0 deletions changelog/+node-not-found-identifier-widened.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`NodeNotFoundError.identifier` is now annotated `Mapping[str, list[str]] | str`. The SDK already raised it with a plain string to name a missing file, so this documents behaviour that was always there; no runtime behaviour changes and no existing caller needs updating.
1 change: 1 addition & 0 deletions changelog/+relogin-non-object-body.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed an `AttributeError` escaping the client when a 401 response carried a body that was valid JSON but not an object, such as the bare array or string a proxy or gateway may return. The silent token refresh now treats any body it cannot read as an envelope as carrying no refresh signal, and the request surfaces `AuthenticationError` as it does for every other unreadable 401.
60 changes: 60 additions & 0 deletions dev/specs/ifc-3034-error-catalogue/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Specification Quality Checklist: Error Catalogue in the Python SDK

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-08-21
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

Two checklist items were resolved by scoping rather than by rewriting, and the reasoning is recorded
here so the plan phase does not relitigate it:

- **"No implementation details" / "written for non-technical stakeholders"** — for a library, the
exception hierarchy *is* the user-facing product, so class names, catalogue codes, and the
transport split are domain vocabulary rather than implementation leakage. The spec names those and
deliberately withholds module layout, file names, generator implementation, and test mechanics.
Recorded as an explicit assumption in the spec rather than left implicit.
- **"Success criteria are technology-agnostic"** — SC-001 through SC-008 are stated as outcomes a
consumer or reviewer can verify (a failure is handleable without reading a message; no string
matching remains; a stale artefact fails validation) rather than as internal mechanics. They do
reference exceptions and catalogue codes, which is unavoidable and correct for this feature.

Two items were originally deferred to the plan and have since been pulled back into the spec, both
prompted by automated review of the pull request:

- **The `identifier` contract on the unified `NodeNotFoundError`.** Deferring the whole question was
wrong: *which* attributes a consumer can read is observable API surface and belongs here, even
though the mechanism does not. FR-016 now pins the contract — every construction shape in use today
keeps working, the server-reported kind and identifier are reachable, one documented accessor works
for both cases, and any type widening is called out in release notes. Surveying the code for this
also turned up that the attribute is *already* heterogeneous: the file handler passes a plain string
where the declared type is a mapping.
- **Multi-error precedence.** FR-013 originally required only that a rule exist, which is untestable
until the rule does. It now specifies that the first error in the response governs, with the
complete list retained, and records why first-*recognised* was rejected: it would make the raised
type depend on binding freshness rather than on the response.
Loading
Loading