# Error codes

Every failure carries `{code, message, hint, docs_url}`. Branch on
`code`. Codes are never renamed or reused; new ones may be added, so
treat an unknown code as `internal`.

| Code | HTTP | FTP | SFTP status | Meaning |
| --- | ---: | ---: | --- | --- |
| `auth_failed` | 401 | 530 | auth refused | Missing or wrong credentials |
| `permission_denied` | 403 | 550 | PERMISSION_DENIED (3) | Logged in, but not allowed |
| `not_found` | 404 | 550 | NO_SUCH_FILE (2) | No such file, directory, user, or session |
| `already_exists` | 409 | 550 | FAILURE (4) | The name is taken |
| `invalid_path` | 400 | 553 | PERMISSION_DENIED (3) | The path leaves the root or is malformed |
| `invalid_command` | 400 | 500 | BAD_MESSAGE (5) | Malformed command, packet, body, or config |
| `rate_limited` | 429 | 421 | FAILURE (4) | A limit fired: ban, cap, timeout, drain |
| `quota_exceeded` | 507 | 552 | FAILURE (4) | The write would exceed the user's quota |
| `tls_required` | 426 | 534 | n/a | TLS is required for this |
| `internal` | 500 | 451 | FAILURE (4) | Server-side fault |

FTP may use a protocol-specific number where an RFC requires one (for
example `504` for an unknown `HOST`, `503` for `HOST` after `USER`); the
code after the number is always one of the ten.

## auth_failed

Credentials were missing or wrong, the password expired, the key is
expired or revoked, the user may not log in under this FTP `HOST`, or
(admin API, MCP) the token is wrong or not configured. All of these look
the same on purpose. Repeated failures from one address lead to a ban
(`rate_limited`).

## permission_denied

The user lacks the permission bit for this path (`r w d n m l`),
possibly because of a directory rule; a config-file user was changed
through the API (edit the file instead); a WebDAV client tried to write
(the gateway is read-only); or an active-mode `PORT` named another host
or a port below 1024. The `hint` says which bit is missing.

## not_found

The file, directory, user, or session does not exist. List the parent
(`MLSD`, SFTP `READDIR`, `GET /api/v1/files`) or the collection
(`GET /api/v1/users`, `GET /api/v1/sessions`).

## already_exists

A create collided with an existing name: `MKD`, an exclusive SFTP open,
SFTP `RENAME` onto an existing file (use `posix-rename@openssh.com` to
replace), or a user name that is taken.

## invalid_path

The path contains NUL, a `..` that leaves the root (also when
percent-encoded), a symlink that points outside, or a new symlink whose
target is outside the root. The same request is
refused identically on every protocol.

## invalid_command

The request is malformed: unknown FTP verb, bad argument, oversized
line or packet, unsupported SFTP extension, invalid JSON, unknown config
key, weak password hash, unsupported SSH key option. The `hint` names
the expected form; `HELP`, `tools/list`, and `/api/v1/openapi.json` list
what is valid.

## rate_limited

A protective limit fired: login ban, connection cap (global or per
address), per-user session cap, too many open SFTP handles, too many
concurrent admin requests, an idle or handshake timeout, or the server
is draining for a restart. Wait and retry; operators tune these under
`[server]`.

## quota_exceeded

The write (upload, SFTP write, or `SETSTAT` that grows a file) would
exceed `quota_bytes`. The partial file is removed unless the client was
extending an existing one (`REST`, `APPE`). Free space or raise the
quota.

## tls_required

The server requires TLS (`require_tls`), `PROT` was sent without `AUTH
TLS`, `CCC` was attempted, active mode was combined with `PROT P`, or a
TLS data connection did not resume the control session while
`require_session_reuse` is on. Connect with `AUTH TLS`, `PBSZ 0`,
`PROT P`, or use the implicit FTPS port.

## internal

A server-side fault: disk error, unreadable TLS material, an S3 or LDAP
server that is down or refusing access. Retry once; if it persists, the
`message` and the daemon log name the cause.
