Skip to content

The Error Registry

The MTHDS standard names every error a runner or a client can meet with a code, and publishes those codes as its core error registry. What the standard leaves to implementations, such as billing, organizations, the hosted catalog, Pipelex's storage, its own configuration and plugins, and the components it ships, Pipelex names with codes of its own under the pipelex extension prefix, which the core registry registers to Pipelex. This page describes how those codes are maintained.

One source, three derived outputs

The Pipelex codes are authored in one public file, pipelex/errors/registry/pipelex-error-registry.yaml, in the MTHDS error registry format. Each entry declares every member its code needs: its title, the domain whose actor must act, its retry verdict, the step for that actor and the step for any other reader, its typed parameters, the templates its sentences are written from, its HTTP status, and what its page says it means and what to do about it.

Everything else is generated from that file, and never edited by hand:

  • pipelex/errors/registry/pipelex-error-registry.json, the registry's JSON snapshot. The pipelex package ships it, load_pipelex_error_registry() in pipelex/errors/registry/registry_document.py reads it into a typed model, and every release attaches it to the GitHub release as an asset.
  • pipelex/errors/registry/pipelex_codes.py, the typed Python of the codes: a PipelexErrorCode member per code, and a closed, frozen parameters model per code, PIPELEX_ERROR_PARAMS mapping each code to its model.
  • The documentation pages under docs/errors/: the Error Codes index and one page per code.

Regenerate them all after changing the source:

make generate-error-registry   # alias: make ger

The generator refuses to write anything from a source with a fault, and prints the faults. A file that does not load is such a fault, printed like the others: a source that is not valid YAML, holds a key twice, holds a key that is a sequence or a mapping, or writes a version, a vendored file that cannot be read or parsed, and a pyproject.toml version the registry cannot be published under. To check without writing, which is what the test suite does as well:

make check-error-registry      # alias: make cer

The test suite fails when any derived file is not what the generator writes, so a change to the source cannot land without its outputs.

The registry's version

The source carries no version. The snapshot takes Pipelex's own version, the [project].version of pyproject.toml, so the number is written in one place and every release publishes the registry under the release's number. A version bump therefore leaves the snapshot and the index page stale until they are regenerated, which the test suite reports: run make generate-error-registry after bumping the version. The release workflow refuses to attach a snapshot whose version is not the release's.

The registry format writes a version as SemVer, while Pipelex's is a PEP 440 version, so a pre-release's number is written in its SemVer form: 0.88.0a1 becomes 0.88.0-a.1, 0.88.0b2 becomes 0.88.0-b.2 and 0.88.0rc1 becomes 0.88.0-rc.1, which SemVer orders as PEP 440 does, while a release's number, such as 0.88.0, is written as it is. The generator and the release workflow's check both take the conversion from pipelex/cli/dev_cli/commands/error_registry/registry_version.py, which needs only the standard library so that the workflow runs it on a bare runner.

The typed codes

Whoever raises a code builds its parameters with the model the registry generates, so that the parameters carry exactly the types the registry declares and none it does not:

from pipelex.errors.registry.pipelex_codes import ConfigVariableMissingParams, PipelexErrorCode

params = ConfigVariableMissingParams(variable="OPENAI_API_KEY", source="env")
assert params.code == PipelexErrorCode.CONFIG_VARIABLE_MISSING
assert params.operator_only_params == frozenset({"variable", "source"})

An identifier or a caller_text is a str, an integer an int, a seconds a float and a bytes an int, neither ever negative, a sha256 a lowercase hexadecimal str, an enum the Literal of its values, and a list a list of its items' type, each item constrained as a parameter of that type is, so a list of bytes refuses a negative item. An optional parameter is its type or None. The models are strict, so a value of another type is refused rather than coerced: a line given as "3" or True is refused, while an integer is accepted where a number of seconds is declared. operator_only_params names the parameters that only whoever runs the runtime may read, which an implementation leaves out of every problem it answers to a caller.

The checks

make check-error-registry and the test suite check the source in three passes, each run only on a document the previous one accepts:

  1. The format's JSON Schema, vendored from the standard, refuses a member missing, unknown or forbidden in its case, and a value outside its closed lists of domains, retry verdicts and step kinds.
  2. The typed model Pipelex reads the registry with refuses the same documents, so that the model can never accept one the schema would not. A member written as null is among them: the format leaves an optional member out rather than writing it as null, and the model refuses a null rather than reading it as an absent member. So is a value of another JSON type: the model is strict, so a status written "402" or an optional written "true" is refused rather than coerced, while a status written 402.0, which JSON Schema counts as an integer, is read as 402. The model refuses one thing more than the schema: a see reference whose path holds a segment made of dots alone, such as .., which the schema's pattern accepts and which would name a page outside the documentation.
  3. The rules a schema cannot state, which the standard applies to its own registry: a family or a code given twice, a code whose family is not declared, a family no code uses, a code or a family outside the pipelex prefix, a template naming a parameter its reader may not see, and a see reference to a documentation page that does not exist. A page under docs/errors/ is the generator's, so a reference there must name a page this registry generates, which a new code may do before the page is written; a page anywhere else must be one this repository holds; and a page that resolves outside docs/ is refused wherever it points. A template may name only a declared parameter that is not optional, and an operator-only parameter only in the actor's step of an operator code.

To these, Pipelex adds the rules its own use of the registry needs:

  • the core registry registers the pipelex prefix under the base URL the pages are published at, https://docs.pipelex.com/errors;
  • every code a sentence names in backticks exists: a core code such as provider.credential_refused in the core registry, a pipelex. code in this one;
  • every parameter name can be a field of the generated parameters model, so it is neither a Python keyword, nor an attribute of a pydantic model, nor a builtin the model's annotations name, such as int or str, which the field would shadow;
  • distinct codes generate distinct names in the typed Python: the generated names drop the prefix and join the segments, so pipelex.run_job.failed and pipelex.run.job_failed would both be RUN_JOB_FAILED and RunJobFailedParams, and the check refuses such a pair rather than let one model replace the other;
  • every title and parameter description can be written into a docstring of the typed Python, so none holds a backslash or a closing quote, and every parameter's field fits on one line of the formatter's length.

The standard's vendored files

The checks read two files of the standard, copied verbatim into pipelex/cli/dev_cli/commands/error_registry/standard/: the registry format's JSON Schema, mthds-error-registry.schema.json, and the core registry's snapshot, mthds-error-registry.json. vendored.toml beside them records the standard's repository, the commit and the version they were copied from, and the SHA-256 digest of each, which a test compares with the files. Nothing at run time reads these copies. To refresh them, copy both files from the standard's release, at the tag v<version> or from its release assets, and update the record.

The development CLI, where the checks and the generators live, is not shipped in the wheel, so neither are these files nor jsonschema, which only the checks use.

The pages and the type URI

A code's type URI is the registry's base URL, a slash, then the whole code: https://docs.pipelex.com/errors/pipelex.billing.credit_exhausted. Its page is docs/errors/pipelex/billing/credit_exhausted.md, one directory per segment but the last, as the MTHDS standard lays out the core codes' pages, which the versioned site serves at /latest/errors/pipelex/billing/credit_exhausted/.

The type URI names no version, and the site is served by GitHub Pages from its gh-pages branch, with no server-side redirect of our own, so the documentation's deployment closes the gap. make docs-deploy-root, which every documentation deploy runs, calls pipelex-dev write-error-type-redirects on its checkout of the gh-pages branch: for every code whose page the latest version holds, it writes errors/<code>/index.html at the site's root, a page that redirects to the code's /latest/ page, and it removes the stub of a code latest no longer documents. While latest documents at least one code, it also writes errors/index.html, which sends the base URL to the Error Codes index under /latest/; before a release that documents the codes is the latest, it writes no stub at all, removes a root stub it wrote earlier, and leaves the base URL to the site's 404.html, which sends it to /latest/errors/ as it sends any path without a version. It only ever removes a file that carries its stubs' marker. GitHub Pages answers a URL naming a directory without its trailing slash, as the type URI does, with a redirect to the same URL with the slash, whatever its last segment holds, and serves the stub there. A code's type URI therefore resolves once a release that documents it is the latest version.

Before the error registry, every PipelexError class had a generated page at errors/<kebab-class-name>/, which its class-derived type_uri named. Those pages are retired, and a report's type URI is now its code's. A client may still hold an old one, from a stored report or a webhook delivery, so the documentation build keeps a static redirect for each retired page in mkdocs.yml's redirect_maps, between the BEGIN retired error class pages and END retired error class pages markers: each is sent to the page of its class's code (the default of a class that raises a set, the fallback of one that takes its cause's code), a core code's page being the standard's, and a page whose class is gone or abstract to the Error Codes index. The keys are the closed record of the retired pages; the targets follow the classes, make generate-error-identity rewrites them, and tests/unit/pipelex/errors/test_retired_error_page_redirects.py fails when they drift (pipelex/errors/retired_error_pages.py). Any other one-segment page under errors/, such as the class-derived type URI of an API-authored error, is sent to the index by the site's 404.html.

Adding or changing a code

  1. Add or edit the entry in pipelex/errors/registry/pipelex-error-registry.yaml, declaring every member. A new family is declared under families, in the order the pages present them.
  2. Run make generate-error-registry, then read the diff of the snapshot, the typed codes and the code's page.
  3. Run make check-error-registry, or the tests under tests/unit/pipelex/errors/registry/.

A registered prefix's codes follow the standard's versioning: a new code or family is an addition, while a changed domain, retry verdict, step kind, status or parameter type changes what the runtime emits and is a breaking change for whoever branches on it, noted as such in the changelog. A reworded title or template is neither.