Skip to content

Refactor CloudKitError to per-serverErrorCode cases with .unknownServerError fallback #358

Description

@leogdion

Context

PR #357 split three of CloudKit's 14 documented `serverErrorCode` values into dedicated `CloudKitError` cases:

  • `.quotaExceeded(reason:, hint: QuotaHint?)` — 413 / `QUOTA_EXCEEDED`
  • `.badRequest(reason:)` — 400 / `BAD_REQUEST`
  • `.atomicFailure(reason:)` — 400 / `ATOMIC_ERROR`

Those three got promoted because they were either a) heavily overloaded at the wire level (`QUOTA_EXCEEDED` carries storage-quota, per-record size, and per-asset size) or b) semantically distinct from each other despite sharing an HTTP status (`BAD_REQUEST` vs `ATOMIC_ERROR`). The remaining 11 server codes still land in `.httpErrorWithDetails(statusCode:, serverErrorCode:, reason:)` with consumers needing string matching.

Proposed work

Enumerate the rest of the documented `serverErrorCode` values as dedicated cases so consumers can pattern-match by intent, and add an `.unknownServerError(code: String, statusCode: Int, reason: String?)` fallback so forward-compat is preserved when Apple adds new codes.

Codes to enumerate (from `openapi.yaml` `serverErrorCode` enum):

Code HTTP Suggested case
`ACCESS_DENIED` 403 `.accessDenied(reason:)`
`AUTHENTICATION_FAILED` 401 `.authenticationFailed(reason:)`
`AUTHENTICATION_REQUIRED` 421 `.authenticationRequired(reason:)`
`CONFLICT` 409 `.conflict(reason:)`
`EXISTS` 409 `.exists(reason:)`
`INTERNAL_ERROR` 500 `.internalServerError(reason:)`
`NOT_FOUND` 404 `.notFound(reason:)`
`THROTTLED` 429 `.throttled(reason:)`
`TRY_AGAIN_LATER` 503 `.tryAgainLater(reason:)`
`VALIDATING_REFERENCE_ERROR` 412 `.validatingReferenceError(reason:)`
`ZONE_NOT_FOUND` 404 `.zoneNotFound(reason:)`

Plus `.unknownServerError(code: String, statusCode: Int, reason: String?)` for codes Apple may add in future spec revisions.

Why this is post-1.0 rather than blocking beta.2

This is a breaking API change that touches:

  • `CloudKitError` enum (add 11 cases, change `httpErrorWithDetails` semantics)
  • `ResponseProcessor` / `CloudKitError+OpenAPI.swift` (dispatch by code)
  • Every consumer that pattern-matches `.httpErrorWithDetails(_, "", _)`
  • Every test that asserts on those patterns

PR #357 took the narrowly-scoped path: split only the three codes where there's either disambiguation value (`QUOTA_EXCEEDED` + `QuotaHint`) or semantic distinction (`ATOMIC_ERROR`). The remaining 11 don't have hint vocabularies and the action a consumer takes on them is roughly uniform (re-auth, back off, retry, surface) so the marginal value of splitting is lower.

Splitting them anyway gives type-safe matching and avoids "what's the current set of codes?" being a string-comparison concern. But it's API churn that doesn't need to land before beta.2.

Verification

  • All existing `.httpErrorWithDetails(...)` pattern-match sites must be migrated to the new cases or to `.unknownServerError`.
  • `MockTransport`-based tests should cover each code's roundtrip through the factory.
  • The `.unknownServerError` path should be exercised with a mock returning a code not in the enum (forward-compat smoke test).

Related: closed sibling work in #356 (pre-1.0 hardening umbrella). Not adding this as a sub-issue of #356 since #356 is for pre-1.0 work; this is post-1.0.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions