Errors¶
How the TwinCell REST API reports failures and how the deeplife SDK maps them to Python exceptions.
Authentication¶
Every request authenticates with the X-API-Key header. Authorization: Bearer … is not accepted and returns 401 (AuthenticationError). The SDK sets the header from api_key= / DEEPLIFE_API_KEY; you only supply it yourself when calling the REST routes directly.
REST error shape¶
Most error responses use FastAPI’s standard JSON body:
Validation failures (422) may return structured detail arrays (Pydantic field errors).
Policy and quota responses often add:
| Header | Meaning |
|---|---|
X-Deeplife-Error-Code |
Machine-readable code (see tables below) |
Retry-After |
Seconds to wait before retry (rate limits) |
x-request-id / x-correlation-id |
Request id for support |
HTTP status → SDK exception¶
| HTTP | SDK exception | Typical cause |
|---|---|---|
| 401 | AuthenticationError |
Missing, invalid, or expired API key |
| 404 | NotFoundError |
Unknown prediction id or artifact not ready |
| 422 | ValidationError |
Dataset / schema validation on upload |
| 429 | See below | Quota, in-flight limit, or rate limit |
| 5xx | ServerError |
Server-side failure |
| Other 4xx | ApiError |
Generic client error |
| Network / timeout | TransportError |
No HTTP response (httpx transport errors) |
All SDK HTTP exceptions inherit from ApiError and expose:
message— APIdetailtextstatus_code— HTTP status when availableerror_code— fromX-Deeplife-Error-Codewhen presentretry_after_seconds— fromRetry-Afterwhen presentuser_message— short text safe for notebooksdebug_message— full diagnostic string for logs
HTTP 429 variants¶
X-Deeplife-Error-Code |
SDK exception | user_message (summary) |
|---|---|---|
rate_limited |
RateLimitError |
Too many requests; wait and retry |
quota_exceeded |
QuotaExceededError |
Prediction quota exceeded for this period |
inflight_limit |
InflightLimitError |
Another prediction already in progress |
external_prediction_quota_exceeded |
QuotaExceededError |
Maximum prediction requests for this account |
external_prediction_quota_disabled |
QuotaExceededError |
Prediction requests disabled for this account |
| (absent on 429) | RateLimitError |
Treated as throttling |
Policy X-Deeplife-Error-Code values (non-429)¶
| Code | HTTP | Meaning |
|---|---|---|
prediction_external_target_validation_only |
403 | Only single-target predictions are available on your account |
prediction_external_split_required |
403 | Pass control and perturbed data as separate files |
prediction_get_job_type_forbidden |
403 | Job type not exposed for external GET |
causal_analysis_internal_only |
403 | Causal analysis endpoint restricted |
These map to ApiError (or ValidationError for 422) with error_code set on the exception.
Two kinds of 403¶
Not every 403 comes from the API. Check the body and the X-Deeplife-Error-Code header before acting:
| Source | Body | error_code |
What it means |
|---|---|---|---|
| API policy | JSON {"detail": …} |
one of the codes above | Your account or key tier may not do this. The request will never succeed as written — change the request (see Limits & quotas). |
| Edge / WAF | HTML page | None |
The request was blocked before reaching the API. Nothing about your key, account, or payload is wrong. |
Because an edge 403 is transient, the SDK retries it on GET-style calls (polling included) using the standard RetryConfig backoff. POST uploads are never retried, and a labelled policy 403 fails immediately — it would never succeed. You therefore only see an edge 403 raised after the retries are exhausted.
At that point there is no JSON to parse, so the SDK raises a bare ApiError with status_code=403, error_code=None, and the generic message "API request failed"; the HTML is preserved on response_text.
from deeplife.twincell import ApiError
try:
final = client.wait_for_prediction(prediction_id=prediction_id)
except ApiError as exc:
if exc.status_code == 403 and exc.error_code is None:
print(exc.debug_message) # includes x-request-id for support
The observed pattern is an edge 403 on GET polling — the multipart upload POST succeeds and returns a prediction_id, then status polls start returning HTML 403 partway through the run. The prediction itself keeps running server-side; only your view of it is blocked.
If you hit this:
- Do not treat it as an auth problem. The same key authenticates fine; the request was blocked at the edge before it reached the API.
- Check your poll rate. The edge allows 30 requests per 60 s per IP, so the SDK's default
poll_interval_secondsof 5 s uses about 40% of the budget. If you lowered it, or you have several waiters (or colleagues) sharing one IP, raise it — see Limits & quotas. - Re-read the result later with
client.get_prediction(prediction_id=…)— the run completes regardless of the blocked polls. Reaching this error means the automatic retries were exhausted, which points at sustained pressure on the IP rather than a brief spike; the default budget (RetryConfig: 3 attempts, up to 8 s apart) covers a short block, not a full 60 s window. - Report it with the
x-request-idfromdebug_messageand the timestamp via Access & Support.
Failed predictions (status: failed)¶
wait_for_prediction() and watch_prediction() return a failed status rather than raising; call status.raise_if_failed() if you want a PredictionFailedError. The API body includes:
error_message— user-facing text (PredictionFailedError.message)error_code— stable worker code when available
status: failed covers two very different situations, and the error_code is what separates them.
Biological outcomes, not faults¶
Three worker codes mean TwinCell ran correctly and concluded that no causal result exists for your target and DEGs. Nothing is broken, and re-running the same inputs cannot produce a result:
error_code |
Billable? | Meaning |
|---|---|---|
target_not_in_interactome |
No | Target absent from the interactome, so no path to it can exist |
degs_not_in_interactome |
No | None of the submitted DEGs mapped, so there is nothing to trace a path from |
target_not_reachable |
Yes | Target is on the interactome, but no causal path reaches it from your DEGs |
target_not_reachable is billable because the model did the work to reach that conclusion.
The API reports these as failed and then 404s every result route (mapped-degs, degs-impacted, causal-paths, causal-graph, intermediary-proteins), which at the HTTP layer is indistinguishable from an unknown prediction id. The SDK resolves that ambiguity for you: TwinCell.get_target_score(), get_all_degs(), get_degs_impacted_by_target(), get_causal_paths(), get_intermediary_nodes(), and plot_causal_graph() all raise NoCausalPathError, which states the conclusion in plain language.
from deeplife.twincell import NoCausalPathError
try:
score = tc.get_target_score(prediction_id=prediction_id)
except NoCausalPathError as outcome:
print(outcome.reason) # why no result exists
print(outcome.error_code) # e.g. "target_not_reachable"
print(outcome.target) # e.g. "TYK2|PROTEIN"
NoCausalPathError is deliberately not an ApiError subclass, so a broad except ApiError around your workflow will not swallow a biological finding as though it were an infrastructure problem. To branch without catching, inspect the status directly:
status = client.get_prediction(prediction_id=prediction_id)
if status.is_biological_outcome:
print(status.outcome_reason)
Genuine failures¶
A failed run with no error_code (or an unrecognised one) is a real fault — a worker crash or an internal error. These keep their original behaviour: result routes raise NotFoundError, and get_target_score() raises ValueError. error_detail carries the traceback for internal accounts.
Constants: ERROR_TARGET_NOT_IN_INTERACTOME, ERROR_TARGET_NOT_REACHABLE, ERROR_DEGS_NOT_IN_INTERACTOME, BIOLOGICAL_PREDICTION_ERROR_CODES, NON_BILLABLE_PREDICTION_ERROR_CODES.
Local (non-HTTP) errors¶
These are raised before or after HTTP and are not in the REST table above:
| Exception | When |
|---|---|
NoCausalPathError |
The run reached a biological conclusion; no causal result exists (see above) |
ValueError |
Missing target on create_prediction_split; missing session state (no prediction id, unknown target row, etc.) |
TypeError |
Wrong argument types or missing required study inputs |
FileNotFoundError |
Local .h5ad path missing (read_h5ad) |
deeplife.twincell.validation.ValidationError |
Local split AnnData checks failed before upload |
botocore.exceptions.ClientError |
S3 download errors when loading remote h5ad |
Related¶
- Limits & quotas — when quota and rate-limit errors occur.
- Entity identifiers —
target_not_in_interactomeand symbol format.