Error Model

All errors follow RFC 7807 application/problem+json:

{
  "type": "https://sfmapi/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Project 'foo' already exists",
  "instance": "/v1/projects"
}

Status Code Mapping

HTTP

Server error class

Caller handling

403

TenantViolationError

Treat as an auth or tenancy failure.

404

NotFoundError

Resource does not exist in the current tenant scope.

409

ConflictError

Retry only after changing the request or idempotency key.

413

QuotaExceededError (storage)

Reduce upload size or increase the storage quota.

422

ValidationError

Fix request fields using the structured errors[] details.

429

QuotaExceededError (rate / GPU seconds)

Back off or reduce concurrent work.

503

Backend unavailable error

Retry when the selected backend/provider is available.

507

StorageError

Free capacity or move to a larger storage backend.

Server-Side Hierarchy

Stable exception classes for sfmapi extensions.

exception sfmapi.errors.BadRequestError(detail='', **extras)[source]

Bases: SfmApiError

400 — request couldn’t be parsed or violates a non-schema invariant.

Use ValidationError (422) when the request shape is well-formed but a field value is wrong; use this when the request couldn’t even be interpreted (e.g., malformed Content-Range, conflicting query params).

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 400
error_type: str = 'bad_request'
title: str = 'Bad request'
exception sfmapi.errors.CapabilityUnavailableError(*, capability, reason='')[source]

Bases: SfmApiError

The requested SfM feature isn’t supported by the current backend.

Returns 501 Not Implemented rather than 5xx because the request itself is well-formed and the server is healthy — it just doesn’t expose the requested capability. The capability extra carries the canonical feature name (see app.core.capabilities) so clients can correlate with GET /v1/capabilities.

Parameters:
  • capability (str)

  • reason (str)

Return type:

None

status_code: int = 501
error_type: str = 'capability_unavailable'
title: str = 'Capability not available in this deployment'
exception sfmapi.errors.ConflictError(detail='', **extras)[source]

Bases: SfmApiError

409 — the request conflicts with the current resource state.

Use for optimistic-concurrency / state-machine violations (e.g., finalizing an upload that’s already finalized, deleting a project with active jobs). Use ValidationError (422) when the request itself is wrong; use this when the request would be valid against a different server state.

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 409
error_type: str = 'conflict'
title: str = 'Conflict'
exception sfmapi.errors.NotFoundError(detail='', **extras)[source]

Bases: SfmApiError

404 — the addressed resource doesn’t exist for this tenant.

Routes MUST raise this rather than returning None so the failure shows up as application/problem+json. Tenant scoping is implicit: a row that exists under a different tenant looks like 404 to the caller (see L2 — multi-tenant boundary).

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 404
error_type: str = 'not_found'
title: str = 'Resource not found'
exception sfmapi.errors.PycolmapUnavailableError(reason='')[source]

Bases: CapabilityUnavailableError

Backwards-compat: the colmap_mod backend can’t load pycolmap.

Subclass of CapabilityUnavailableError so callers that catch the parent see this too. Status code stays 501 — the failure is the same shape (a capability the deployment doesn’t expose), even though the reason is backend-specific.

Parameters:

reason (str)

Return type:

None

error_type: str = 'pycolmap_unavailable'
title: str = 'pycolmap not available in this deployment'
exception sfmapi.errors.QuotaExceededError(detail='', **extras)[source]

Bases: SfmApiError

429 — caller hit a per-tenant or per-request rate / size cap.

Currently fires on oneshot request-body caps (oneshot_max_request_bytes); Phase 5 plumbs in fair-share scheduling + per-tenant job quotas through this same error. SDK ergonomics shims interpret 429 as retryable when retry_after is set.

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 429
error_type: str = 'quota_exceeded'
title: str = 'Quota exceeded'
exception sfmapi.errors.SfmApiError(detail='', **extras)[source]

Bases: Exception

Base class for every domain error sfmapi raises.

Subclasses MUST set status_code (HTTP) and error_type (the slug used to build type: https://sfmapi.github.io/errors/<slug> in the RFC 7807 envelope). The FastAPI exception handler in app/main.py::sfmapi_error_handler turns these into application/problem+json responses; never raise raw HTTPException from services — keep the wire shape consistent.

Free-form extras are merged into the envelope as top-level keys (capability on 501, retry_after on 429); see the typed surface in ProblemResponse.

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 500
error_type: str = 'internal'
title: str = 'Internal error'
as_problem(instance=None)[source]
Parameters:

instance (str | None)

Return type:

dict[str, Any]

exception sfmapi.errors.StorageError(detail='', **extras)[source]

Bases: SfmApiError

507 Insufficient Storage — the backing store rejected a write.

Used for blob-store write failures, snapshot rename collisions, workspace-out-of-space conditions. Distinct from a generic 500: the request itself was valid, the storage layer couldn’t hold it.

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 507
error_type: str = 'storage'
title: str = 'Storage error'
exception sfmapi.errors.TenantViolationError(detail='', **extras)[source]

Bases: SfmApiError

403 — tenant scoping was bypassed or auth resolution failed.

Raised by app.core.tenancy.current_tenant() when the Authorization header is missing / unrecognized, and by repository helpers that detect a tenant_id mismatch. See L2 in decisions.md (multi-tenant from day 1, default until auth lands). Once real auth ships, 401 (missing) and 403 (rejected) will split — for now both surface here.

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 403
error_type: str = 'tenant_violation'
title: str = 'Tenant boundary violation'
exception sfmapi.errors.ValidationError(detail='', **extras)[source]

Bases: SfmApiError

422 — semantic validation failed after parsing succeeded.

Distinct from FastAPI’s fastapi.RequestValidationError (Pydantic schema-shape failures): this fires from inside services when a value is well-typed but rejected on a domain rule (cross-field consistency, foreign-key lookup, plausibility). Both paths emit the same RFC 7807 problem+json shape — see L19 for the structured errors[] invariant.

Parameters:
  • detail (str)

  • extras (Any)

Return type:

None

status_code: int = 422
error_type: str = 'validation'
title: str = 'Validation error'

Client packages map this RFC 7807 envelope into their own exception types. Keep SDK-specific exception imports in the sfmapi-sdk repository so this server reference stays focused on the wire contract.