Repository navigation
Failure Codes
Every failed document carries one of sixteen stable codes, stored in Document.errorCode, with a fixed sentence written for a person in Document.error.
Canonical source: docs/failure-codes.md · code: lib/workflows/failure.ts.
-
The message a user sees always comes from the table below, never from the thrown error. The raw text is read only to pick a code and is then discarded. This matters beyond tidiness: a parser or driver error is not guaranteed to be free of document content, and invariant 6 says nothing logs or stores document content. (Before #62, the screen showed things like
FatalError: Unsupported file type, or a Prisma error.) -
retryableanswers one question: could running the same pipeline again, right now, produce a different result? An unsupported file type does not become supported on the second attempt, and a button that cannot work is worse than no button.
The module imports nothing, so the browser can classify a stored code without pulling the server in behind it.
| Code | Meaning | Retryable | What to do |
|---|---|---|---|
unsupported-type |
Not a format Anonify can read. The supported list is generated from the format register. | No | See Supported Formats. |
extension-mismatch |
The contents do not match the extension, e.g. a .pdf that is really a zip. |
No | Check the file is what its name says. Magic bytes decide, not the name. |
empty-file |
Zero bytes. | No | Upload the real file. |
too-large |
Larger than MAX_UPLOAD_BYTES (50 MiB). |
No | Split it, or raise the ceiling in lib/config.ts on your own instance. |
corrupt-source |
The stored copy no longer matches the checksum taken at upload, so it was not read. | No | Upload again. If it recurs, your storage is returning different bytes than it was given. |
missing-upload |
The upload vanished before ingest, or the row is gone. | No | Upload again. |
too-complex |
More messages, parts, nesting or content than this instance will read or expand. Refused whole, because a partly processed file would look complete and would not be. | No | An administrator can raise the EML parser, attachment-expansion or mailbox limits (Configuration). |
empty-container |
A mailbox with bytes in it but no readable messages. Distinct from empty-file: telling somebody their 30 MiB archive is empty would send them looking for the wrong problem. |
No | Check the export; it may not be mbox. |
quota |
The document needs more of today's allowance than is left. The allowance resets at midnight UTC. Not retryable even so: a retry pressed before the reset spends a rate-limit token to fail the same way. | No | Wait for the reset. Self-hosted installs have no quotas; if you see this on your own machine, check ANONIFY_PROFILE. |
rule-too-broad |
A batch or global rule, carried into the document at the end of processing, matched too much (over 10,000 matches or 5 s of matching), so none of it was applied here. The document failed after extraction, so it still opens for review. | Yes | Narrow the rule or switch it off in the Rules panel, then retry. |
internal-state |
Processing stopped partway and cannot resume. Also the fallback for an unrecognised FatalError the pipeline threw itself. |
No | Upload again; report it if it recurs. |
configuration |
The instance is not fully configured, e.g. OCR_PROVIDER=mistral with no MISTRAL_API_KEY. Needs an administrator, not a retry. |
No | Fix the environment and restart. A misspelled Mistral key (e.g. MISTRAL_AI_KEY) is named in the log. |
storage |
The file store could not be reached. Usually temporary. | Yes | Retry. |
database |
The database could not be reached. Usually temporary. | Yes | Retry. |
timeout |
The work took longer than allowed. Usually temporary. | Yes | Retry; on Vercel Hobby, see the step limits in Deployment. |
unknown |
Anything the matchers do not recognise. The original file was not modified. | Yes | Retry; report it if it recurs. |
A document that failed before the code column existed has no code at all. Those stay retryable, because that is the old behaviour and a retry is cheap.
describeFailure does not read the error's class or stack. It reads the message text and walks an ordered list of regexes:
| Code | Matches |
|---|---|
extension-mismatch |
contents do not match its extension |
unsupported-type |
unsupported file type · no extractor registered
|
empty-file |
file is empty |
too-large |
file is too large |
corrupt-source |
checksum mismatch |
too-complex |
exceeds the … (expansion) limit of |
empty-container |
no messages found in this mailbox |
missing-upload |
no upload to ingest · document no longer exists
|
quota |
daily demo limit reached |
internal-state |
has not been ingested/normalized |
configuration |
environment variable · must decode to · is not configured · SOME_VAR is not set · OCR_PROVIDER
|
database |
database_url · econnrefused · prisma · connection pool/terminated
|
storage |
blob · s3 · fetch failed · enoent · no such file
|
timeout |
timeout · etimedout · timed out · aborted
|
Ordering is load-bearing. The pipeline's own specific messages come before the broad infrastructure patterns, so "no such file" is read as storage only once it is known not to be one of the specific cases. Anything that matches nothing is unknown. rule-too-broad is set directly by the rule-carry step.
too-complex is deliberately worded to cover the MIME parser's limits, attachment expansion's and the mailbox's. To the person holding the file, they are one refusal with one remedy.
The error that crosses the durable boundary is not the instance that was thrown. Its class name is folded into the message, and the wrappers nest:
FatalError: Step "…//extractAndNormalize" failed after 3 retries: <cause>
unwrap peels them in a bounded loop (depth 4) and returns:
-
message: the cause underneath, which is what gets classified; -
fatal: true only when aFatalErrorprefix was seen and no "failed after N retries" wrapper was; -
retried: true when the step exhausted its retries.
A step that exhausted its retries is re-thrown as a FatalError too, because from the runtime's side there is nothing left to try. But the cause underneath was transient enough to be retried, and reporting it as a verdict would remove the retry button in exactly the case that wants it. That is why fatal is not simply "the message said FatalError".
An unrecognised FatalError is still fatal. The pipeline throws one only where it has decided retrying is pointless, so it maps to internal-state, not unknown.
| Function | Does |
|---|---|
describeFailure(error) |
unwrap → classify → { code, message, retryable }; what the fail step calls |
failureForCode(code) |
the stored code → its message and retryability; unknown → unknown
|
isRetryable(code) |
whether to offer Retry; no code → true
|
-
Document.error/Document.errorCode, and thedocument.failedstream event'spayload: { code, retryable }. -
The workspace: failed before extraction shows the failure screen; failed after extraction opens the editor with a
FailureNoticebanner. - The documents list and batch view: a Retry button, only for retryable codes.
-
POST /api/documents/:id/retry: refuses a non-retryable code with409andretryable: false.
Export failures are separate: a verification refusal is 500 "did not pass verification and was not saved", and batch-export skips have their own named reasons (Batches and Mailboxes).
Anonify: AI proposes, the application applies, and only what a person accepted is removed.
Repository · Report a bug · Request a feature · Security policy · Apache-2.0
This wiki describes v1.10.0 and summarises and links the in-repo documentation. Where the two disagree, the files under docs/, CHANGELOG.md and SECURITY.md are canonical.
Getting going
Using it
- Supported Formats
- Review Workspace
- Search and Rules
- Hush Assistant
- Redaction Presets
- Export and Anonymisation
- Batches and Mailboxes
How it works
- Architecture
- Processing Workflow
- Redaction Engine
- Storage and Encryption
- Streaming IO
- Data Model
- API Reference
- Security Model
When it breaks
Contributing
Repository · Issues · Releases · Changelog