diff --git a/crates/didbot-serve/src/error.rs b/crates/didbot-serve/src/error.rs index 427bb791..e1373e63 100644 --- a/crates/didbot-serve/src/error.rs +++ b/crates/didbot-serve/src/error.rs @@ -268,6 +268,31 @@ impl From<&ProvisionError> for ApiError { | ProvisionError::Record(RecordError::StorageFull { .. }) => { (StatusCode::INSUFFICIENT_STORAGE, "StorageFull") } + // 409, and its own name, before the general store arm below it. + // A DID this deployment already holds is the one store refusal + // that is not a store failure: nothing is unwell, nothing was + // written, and the request will be refused the same way forever + // because `insert` is not an upsert (see + // [`didbot_pds::StoreError::AlreadyExists`]). A 500 tells a + // caller to back off and retry, which is exactly the wrong move + // -- it is the same posture `StorageFull` is carved out of this + // arm for, and the same 409 `HandleTaken` and + // `InvalidAccountTransition` report for a well-formed request + // that collides with state already here. + // + // `AccountAlreadyExists` names no error the lexicons declare, + // the same position `RateLimitExceeded` and `ServerNotReady` are + // in: no `com.atproto.*` lexicon has a name for this because + // upstream mints a DID nobody asked for rather than one derived + // from a caller's `agentId`, so the collision cannot arise there. + // `DuplicateCreate` is declared, but only by + // `com.atproto.server.getAccountInviteCodes` and only about + // invite codes; reusing it here would tell a client something + // false. 409 is the standard status and every client already + // knows it, and the name is there for the ones that switch on it. + ProvisionError::Store(didbot_pds::StoreError::AlreadyExists { .. }) => { + (StatusCode::CONFLICT, "AccountAlreadyExists") + } ProvisionError::Store(..) => (StatusCode::INTERNAL_SERVER_ERROR, "StoreFailed"), // 400: the caller's, and fixable by the caller, which is what // separates it from `StorageFull` above. `RecordTooLarge` is