Back to Fhevm

HTTP API Design

relayer/docs/http-api-design.md

0.13.0-06.9 KB
Original Source

HTTP API Design

High-level description of the V2 HTTP API contract. For the generated spec see openapi.yml.


Async POST + GET pattern

Every operation follows a two-step flow:

  1. POST submits the request — returns 202 Accepted with a jobId (UUID) and a Retry-After header
  2. GET waits until Retry-After polls by jobId — returns 202 (still processing), 200 (completed), or an error code

Read-only endpoints (GET /v2/keyurl, health checks) respond synchronously.

Uniform response envelope

All V2 responses share a single format:

json
{
  "status": "queued | succeeded | failed",
  "requestId": "<uuid>",
  "result":    { /* endpoint-specific */ } | null,
  "error":     { /* error object */     } | null
}
  • status is always present — the outcome can be determined from the body alone, independent of the HTTP code
  • result and error are mutually exclusive; the unused field is null

Structured errors with scoped labels

Error responses carry a machine-readable label and a human-readable message:

json
{
  "label":   "validation_failed",
  "message": "Validation failed for 2 fields in the request",
  "details": [
    { "field": "contractAddress", "issue": "Must be 0x + 40 hex chars" }
  ]
}
  • details array is present only for validation_failed and missing_fields
  • Labels are drawn from a fixed canonical set (16 total), but not every label applies to every endpoint or HTTP code — they are scoped per endpoint
  • See ../openapi.yml for the full per-endpoint matrix

HTTP status code semantics

CodeMeaning
200Completed (GET success, keyurl)
202Accepted / still processing
400Client error (bad JSON, missing fields, validation, unsupported chain, ACL)
404Job ID not found
429Rate limited (queue full) — includes Retry-After
500Internal error
503Unavailable (timeout, paused contract, insufficient balance, gateway unreachable)

Request deduplication

Requests are SHA-256 hashed into an internal job ID (int_job_id). The database enforces uniqueness on active requests via a partial unique index.

  • Identical concurrent POSTs return the same ext_job_id — no duplicate processing
  • Duplicate of a completed request returns the stored result immediately
  • See ID.md for full identifier semantics

Three IDs

IDExposedFormatPurpose
int_job_idNoSHA-256 hashContent-based deduplication
ext_job_idYesUUID v4User facing, references an operation across requests.
requestIdYesUUID v7Per-HTTP-call logging/correlation

ext_job_id is stable across retries of the same content; requestId is fresh on every HTTP call and not stored. See ID.md for detailed relationships.

Dynamic Retry-After

Both POST (202) and GET (202) responses include a Retry-After header computed from real-time queue depth and drain rate — not a fixed value.

  • Format: relative seconds for both 429 and 202
  • POST: estimated from queue depth at submission time
  • GET: estimated from the request's current position and processing stage
  • See dynamic-retry-after-design.md for formulas and examples

Business outcomes vs errors

A completed request can carry a negative business result that is not an error.

  • Input proof accepted: false returns 200 OK with status: succeeded
  • The error envelope is reserved for actual failures (client mistakes, server faults, unavailability)

Early validation before queuing

Chain ID support and host ACL checks run before the request enters any queue, so invalid requests don't consume queue capacity. These return 400 immediately.

Request lifecycle

POST /v2/{endpoint}
  ├─ Parse + validate ──→ 400 (reject early)
  ├─ Dedup check
  │   ├─ Duplicate completed ──→ 200 (return stored result)
  │   └─ Duplicate processing ──→ 202 (reuse existing job_id)
  └─ New request ──→ 202 (queue + return job_id)

GET /v2/{endpoint}/{job_id}
  ├─ Not found ──→ 404
  ├─ Still processing ──→ 202 + Retry-After
  ├─ Completed ──→ 200 + result
  └─ Failed ──→ 4xx/5xx + error label

For internal status transitions (queued → processing → tx_in_flight → …), see status-transitions.md.

V2 endpoints

MethodPathAsyncNotes
POST/v2/public-decryptYesChain ID validated early
GET/v2/public-decrypt/{job_id}Yes+ host ACL + revert classification
POST/v2/user-decryptYesChain ID validated early
GET/v2/user-decrypt/{job_id}YesShare threshold required
POST/v2/delegated-user-decryptYesChain ID validated early
GET/v2/delegated-user-decrypt/{job_id}YesSame GET handler as user-decrypt
POST/v2/input-proofYesNo chain ID / ACL validation
GET/v2/input-proof/{job_id}Yesaccepted: false is 200, not error
GET/v2/keyurlNoRead-only; waits up to 5s on first call

Operational endpoints

PathPurpose
/livenessAlways 200 — for k8s liveness probe
/healthz200/503 — dependency health check
/versionBuild version and git SHA
/metricsPrometheus metrics
DocumentCovers
../openapi.ymlPer-endpoint HTTP codes, labels, revert mapping
ID.mdAll identifier types and relationships
status-transitions.mdInternal request state machine per endpoint
dynamic-retry-after-design.mdRetry-After computation formulas and examples