OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

swift — Errors

Errors — swift — branch stable/2026.2

Branch stable/2026.2 @ a1bdaeba21bd — repository https://opendev.org/openstack/swift — analysed on 2026-10-29

Swift returns HTTP statuses, not error codes: there is no JSON error body and no business code. What a client sees is decided in three places: the validation helpers and middlewares (4xx before any backend call), the storage servers (statuses of each replica) and the proxy's quorum logic, which turns N backend statuses into one client status. Exceptions are internal: they are converted to statuses inside each server and never cross the wire as such. The S3 API (s3api) is the exception: it returns S3 XML errors with codes.

1. Error body

LayerBodySource
Swift API (proxy and storage servers)default: <html><h1>{title}</h1><p>{explanation}</p></html> (Content-Type: text/html; charset=UTF-8), from RESPONSE_REASONS; many validation errors set a short text/plain body instead (Invalid Content-Type, Metadata name too long: …)swift/common/swob.py:1469, swift/common/swob.py:59
HEAD responsesno body, same statusswob
Unhandled exception in the proxy500 Internal Error, logged ERROR Unhandled exception in requestswift/proxy/server.py:631
Exception escaping the proxy WSGI entry point500 Server Error, body Internal server error.\nswift/proxy/server.py:517
Unhandled exception in a storage server500 with the Python traceback as body (internal only)swift/obj/server.py:1452
catch_errors (outermost)catches anything raised below and answers 500; also enforces that the body length matches Content-Length (logs Client disconnected)swift/common/middleware/catch_errors.py:150
S3 API<?xml …?><Error><Code>NoSuchKey</Code><Message>…</Message><Resource>…</Resource><RequestId>tx…</RequestId></Error>; code is the class nameswift/common/middleware/s3api/s3response.py

Clients parse statuses, not texts. python-swiftclient raises ClientException(http_status, http_reason, http_response_content) and retries 5xx, 408, 499 and (by default) 498/429; it re-authenticates once on 401 (python-swiftclient:swiftclient/client.py:1773). Glance's store maps 404 → NotFound, 409 → Duplicate, 413 → StorageFull (glance_store:glance_store/_drivers/swift/store.py:1129). See 07 § 4.

2. How the proxy chooses the status: quorum

For writes and for HEAD/GET of accounts and containers the proxy collects one status per backend node and calls best_response (swift/proxy/controllers/base.py:2225):

  1. Statuses are grouped by class 2xx, 3xx, 4xx (swift/proxy/controllers/base.py:2272). The first class, in that order, that reaches the quorum wins; within it the highest status is returned with its reason and body.
  2. Quorum: floor(n/2)+1 for account/container rings and replicated policies (swift/common/utils/__init__.py:2550, swift/common/storage_policy.py:438); ec_ndata + min_parity_fragments_needed() for EC PUTs (swift/common/storage_policy.py:637).
  3. If no class reaches the quorum, the proxy answers 503 and logs <type> returning 503 for <statuses> (swift/proxy/controllers/base.py:2267).
  4. overrides lets some statuses count as others: for DELETE, a 404 is turned into 204 when a quorum is otherwise impossible; for container PUT, 202 and 201 mix.

Consequences that clients observe (and a rewrite must reproduce):

  • 5xx never wins a quorum by itself: 3 nodes answering 507, 507, 201 give 503, not 507.
  • A 404 on GET object is returned only when every primary (and the handoffs tried) answered 404; one node holding a tombstone newer than another node's data makes the proxy return 404 because the newest timestamp wins (X-Backend-Timestamp).
  • A POST with mixed 404 and 202 where a replica proved the object exists gives 503 (swift/proxy/controllers/obj.py:961).
  • A node that keeps failing is error-limited: after error_suppression_limit (10) errors within error_suppression_interval (60 s) it is skipped (swift/proxy/server.py:229, swift/common/error_limiter.py:33). A 507 marks a node error-limited at once (swift/proxy/controllers/obj.py:783).

3. Client-facing statuses

StatusSituationRaised at
400path version other than v1/v1.0swift/proxy/server.py:563
400negative Content-Length (Invalid Content-Length)swift/proxy/server.py:548
400metadata name empty / too long, value too long, too many, total too largeswift/common/constraints.py:144–swift/common/constraints.py:169
400object name longer than 1024 bytesswift/common/constraints.py:207
400Content-Type with forged swift_bytes or invalid (Invalid Content-Type)swift/common/constraints.py:222
400X-Delete-After / X-Delete-At non-integer or in the pastswift/common/constraints.py:330, swift/common/constraints.py:351
400unknown or deprecated X-Storage-Policy on container PUTswift/proxy/controllers/container.py:69
400invalid ACL (X-Container-Read/Write, X-Account-Access-Control)swift/proxy/controllers/container.py:87
400SLO manifest invalid (per-segment error list in the body)swift/common/middleware/slo.py:395
400name rejected by name_checkswift/common/middleware/name_check.py:117
401no or invalid token, invalid temp URL / form signatureswift/common/middleware/keystoneauth.py:588, swift/common/middleware/tempurl.py:787
403authenticated but not allowed (ACL, role, read-only account); max_containers_per_account reached; Invalid host headerswift/common/middleware/keystoneauth.py:401, swift/proxy/controllers/container.py:664, swift/proxy/server.py:574
404account / container / object absent, object expired, path not splittableswift/proxy/server.py:568, swift/obj/server.py:1202
405method not allowed on this resource (Allow header); account PUT/DELETE without allow_account_management; writes under read_onlyswift/proxy/server.py:595, swift/proxy/controllers/account.py:113
406listing Accept not satisfiableswift/common/middleware/listing_formats.py:40
408client too slow sending the body (client_timeout, default 60 s)swift/proxy/controllers/obj.py:1168
409container DELETE on a non-empty container; container PUT changing the policy; object PUT with a timestamp not newer than the stored one (internal X-Timestamp reuse)swift/container/server.py:355, swift/proxy/controllers/obj.py:726
411object PUT with neither Content-Length nor chunked encodingswift/common/constraints.py:175
412If-None-Match: * on an existing object; If-Match mismatch; invalid UTF-8 path; Bad URL; listing limit > 10000; bad Destination headerswift/proxy/controllers/obj.py:724, swift/proxy/server.py:551, swift/common/middleware/copy.py:148
413object larger than max_file_size; container or account quota exceeded; SLO manifest too largeswift/common/constraints.py:197, swift/common/middleware/container_quotas.py:65, swift/common/middleware/slo.py:1362
416Range not satisfiableswob
422ETag header does not match the body MD5 (object server), or replicas returned mismatched ETagsswift/obj/server.py:1019, swift/proxy/controllers/obj.py:1226
498rate limited (ratelimit, Slow down)swift/common/middleware/ratelimit.py:232
499client disconnected before sending the whole bodyswift/proxy/controllers/obj.py:1153
500unhandled exceptionswift/proxy/server.py:631
503no quorum; not enough connectable nodes for the PUT (Object PUT returning 503, n/m required connections); container info lookup failed; unknown policy index; a shard of a sharded container failed during listingswift/proxy/controllers/obj.py:819, swift/proxy/server.py:480, swift/proxy/controllers/container.py:572

4. Storage-server statuses (internal wire)

These are what a Go storage node must return to Python proxies and daemons (and what a Go proxy must interpret):

StatusSituationSource
201 / 202 / 204create / accepted (POST, DB updates) / deleteeach server
400invalid path, missing or invalid X-Timestamp (Missing X-Timestamp header), bad X-Delete-At, MIME errors (no MIME boundary, couldn't find PUT commit MIME doc, no Content-MD5 in footer, invalid JSON for footer doc)swift/common/constraints.py:293, swift/obj/server.py:916, swift/obj/server.py:568, swift/obj/server.py:597
404object/DB absent; for objects the response carries X-Backend-Timestamp (tombstone time) and X-Backend-Durable-Timestamp so that the proxy can compareswift/obj/server.py:1202
409PUT/DELETE/POST older than or equal to what is stored, with X-Backend-Timestamp; container DELETE when not empty; container PUT changing the policyswift/obj/server.py:874, swift/container/server.py:355
412If-None-Match hit on PUT; X-If-Delete-At mismatch on expirer DELETEswift/obj/server.py:867, swift/obj/server.py:1354
403DiskFileCollision: the name in the metadata of the data file is not the requested name (hash collision or corruption)swift/obj/server.py:1444
408 / 499body read timeout / client disconnectswift/obj/server.py:1115, swift/obj/server.py:1048
422footer MD5 or ETag mismatchswift/obj/server.py:599, swift/obj/server.py:1019
500unhandled exception, traceback in the body; multiphase commit missing (HTTPServerError)swift/obj/server.py:1452, swift/obj/server.py:1033
503DiskFileStateChanged during GET; replication concurrency limit reached (replication_concurrency)swift/obj/server.py:1209
507device not mounted (mount_check), no xattr support, no space (ENOSPC, EDQUOT)swift/obj/server.py:852, swift/obj/server.py:857, swift/account/server.py:134
529backend_ratelimit per deviceswift/common/middleware/backend_ratelimit.py:198

5. Exception catalogue (internal)

Exceptions are defined in swift/common/exceptions.py and converted by the server that catches them. The ones that change a status or a behaviour:

ExceptionHierarchySituationWhat the caller sees
DiskFileNotExistDiskFileError (swift/common/exceptions.py:80)no .data/.ts for the hash404
DiskFileDeletedDiskFileNotExist (swift/common/exceptions.py:92)newest file is a tombstone; carries its timestamp404 with X-Backend-Timestamp (swift/obj/server.py:1307)
DiskFileExpiredDiskFileDeleted (swift/common/exceptions.py:100)X-Delete-At passed404 (swift/obj/server.py:1303)
DiskFileQuarantinedDiskFileError (swift/common/exceptions.py:72)metadata unreadable, size or MD5 mismatch while reading: the hash directory is moved to <dev>/quarantined/objects[-N]/404; replication will restore it (swift/obj/diskfile.py:350)
DiskFileCollision(swift/common/exceptions.py:76)name in metadata ≠ requested path403 (swift/obj/server.py:1444)
DiskFileNoSpace(swift/common/exceptions.py:104)ENOSPC/EDQUOT writing data or xattrs507 (swift/obj/diskfile.py:297)
DiskFileXattrNotSupported(swift/common/exceptions.py:112)filesystem without user xattrs (ENOTSUP)507 (swift/obj/diskfile.py:209)
DiskFileBadMetadataChecksum(swift/common/exceptions.py:116)user.swift.metadata_checksum ≠ MD5 of the picklequarantine (swift/obj/diskfile.py:238)
DiskFileDeviceUnavailable(swift/common/exceptions.py:108)device not mounted / not a directory507
DiskFileStateChanged(swift/common/exceptions.py:84)file vanished between listdir and open503 on GET, 404 elsewhere (swift/obj/diskfile.py:214)
PathNotDirOSError (swift/common/exceptions.py:132)suffix directory missing while hashing: the suffix is dropped from hashes.pkl— (swift/obj/diskfile.py:1387)
LockTimeout, ReplicationLockTimeout, PartitionLockTimeoutMessageTimeout (swift/common/exceptions.py:168).lock file of a partition or DB directory not obtained in timeDB servers: 500/HTTPConflict; SSYNC: error line
DatabaseConnectionErrorsqlite3.DatabaseError subclass (swift/common/db.py)DB missing or skip_commits404 / 500 (swift/account/server.py:116)
ChunkReadTimeout, ChunkWriteTimeout, ConnectionTimeoutTimeout (swift/common/exceptions.py:148)client_timeout, node_timeout, conn_timeout expired408, or the node is skipped (error-limited)
ChunkReadError(swift/common/exceptions.py:140)client socket closed499
InsufficientStorage(swift/common/exceptions.py:44)a node answered 507 during PUT connectnode error-limited, next handoff tried (swift/proxy/controllers/obj.py:782)
ListingIterError, SegmentError(swift/common/exceptions.py:204, swift/common/exceptions.py:218)a DLO/SLO segment is missing or changed after headers were sentthe response is truncated and the connection closed (the status was already 200)
RingLoadError, RingValidationError(swift/common/exceptions.py:172)invalid ring at startupprocess does not start; at reload the old ring is kept (swift/common/ring/ring.py:344)
ReplicationException(swift/common/exceptions.py:226)SSYNC protocol violation, unexpected statuslogged by the replicator/reconstructor, job retried next pass
APIVersionError(swift/common/exceptions.py:242)invalid version prefix400
EncryptionException, UnknownSecretIdError(swift/common/exceptions.py:246)crypto metadata unreadable, unknown key id500 on GET (logged)

The inventory (_inventory/inventory_stable-2026.2.json) lists all 196 exception classes found, including the 60 s3api error classes and the CLI ones.

6. S3 API errors

s3api converts Swift statuses into S3 errors (swift/common/middleware/s3api/s3response.py): 404 on a container → NoSuchBucket, on an object → NoSuchKey; 409 on container DELETE → BucketNotEmpty; 401/403 → AccessDenied, SignatureDoesNotMatch, InvalidAccessKeyId, RequestTimeTooSkewed; 413 → EntityTooLarge; 400 from SLO for small parts → EntityTooSmall; unknown → InternalError. Each class sets its _status and _msg; extra elements (BucketName, Key…) come from constructor kwargs. The functional run recorded the combinations actually returned: _rewrite/observed-errors_stable-2026.2.md.

7. Errors outside the request path

WhereSituationEffect, log
object server, container updatecontainer server unreachable or non-2xxERROR Container update failed (saving for async update later); async pending written (swift/obj/server.py:339)
object updaterasync pending cannot be sentkept, retried next pass; after a 404 of the root container and redirect → follows container_path (swift/obj/updater.py:407)
replicator / reconstructorremote 507node skipped; Remote drive not mounted / handoff used
replicatorhashes.pkl corruptUnable to read %r warning, full rehash (swift/obj/diskfile.py:1347)
auditor (object)MD5/size/metadata mismatchERROR Object … failed audit and was quarantined; quarantined, quarantines stat in recon
DB broker, DB auditorSQLite reports a corrupt or malformed database (database disk image is malformed, no such table…)DB moved to <dev>/quarantined/<type>s/<hash> and the error re-raised (swift/common/db.py:502, swift/common/db.py:479)
DB broker.pending entry not unpicklableInvalid pending entry %(file)s: %(entry)s, entry dropped (swift/common/db.py:849)
shardercleaving or shard-range replication incompleteretries next cycle; container stays in sharding state; warnings every periodic_warnings_interval
expirerDELETE answered 412 (object rewritten without the same X-Delete-At)queue entry removed, object kept
ring reloadnew ring invalidold ring kept silently (swift/common/ring/ring.py:344)

Source: reverse-spec-swift/02-errors_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.