Error handling¶
What the spec tells us (and what it doesn't)¶
Success responses are 201 for creates, 204 for transitions/deletes, 200
for reads. Error responses are JSON with a message (usually a code too); the
shapes below are from stage. Log the message — it names the offending field.
Error bodies are JSON with a message, and usually a code:
{ "code": 404, "message": "Project (999999999) not found" }
{ "message": "Invalid API token provided" }
The message is genuinely useful — for a bad enum it even lists the allowed
values:
{ "code": 400, "message": "Invalid JSON: [textType] Does not have a value in the enumeration [\"specialized-text\",\"product\",...]" }
How errors surface¶
Guzzle throws on 4xx and 5xx by default (http_errors is on), so a failed call is
an exception, not a silent bad return:
use GuzzleHttp\Exception\ClientException; // 4xx
use GuzzleHttp\Exception\ServerException; // 5xx
use GuzzleHttp\Exception\RequestException; // base (also network/timeout)
try {
$item = $client->createItem(...);
} catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode();
$body = (string) $e->getResponse()->getBody();
// log $body — it usually names the offending field. Don't retry a 4xx.
} catch (RequestException $e) {
// network / timeout / 5xx — retryable with backoff
}
Reading the status code¶
| Code | Means | Retry? |
|---|---|---|
400 |
Bad payload — malformed JSON, missing required field, or a value outside an enum (bad textType, unmapped language). Message: Invalid JSON: [field] ... |
No — fix the request |
401 |
Missing/invalid token, or a stage key against prod (or vice versa). Message: Invalid API token provided |
No — fix auth |
403 |
Authenticated but not allowed to touch this resource | No |
404 |
No such project/item id. Message: Project (123) not found |
No (unless you raced a delete) |
5xx |
Server-side / transient | Yes — backoff and retry |
Note: on stage, semantic rejections (bad enum value, unmapped language code) come
back as 400, not 422 — the API folds validation into "Invalid JSON". So
400 covers both "your JSON is broken" and "your value isn't allowed"; read the
message to tell them apart.
Catch the common ones before you send¶
Most failures are self-inflicted 4xx, and several are cheaper to catch client-side than to round-trip. The client already does this for:
- Empty project name →
InvalidArgumentException(the API requires a non-empty name anyway). - Unknown project
type→InvalidArgumentException(onlyorder/quote). - Unmapped language code →
LanguageMap::toEurotext()throws before the request. - Invalid
__metavalue →InvalidArgumentException(flat key-value only).
textType is intentionally not validated client-side — see
06-text-types.md for why (it would drift). Validate it
against getTextTypes() if you want to fail early.
Retries¶
The client doesn't retry for you — the right policy depends on your stack. Reads
(GET /project, GET /item) are safe to retry freely. transitionProject and
the info endpoints are safe too. Be careful re-running
createProject/createItem blindly so you don't create duplicates — check what
landed first or lean on cleanup (12-idempotency.md).
If you want automatic retries, Guzzle's RetryMiddleware slots into the handler
stack you already pass into the constructor — add it there rather than wrapping
every call.
Cleanup on partial failure¶
Delete works here because it runs immediately on failure, while the project is
still a draft or freshly transitioned. Once the items are in-progress the
project can no longer be deleted — so clean up at the point of failure, not
later.
try {
$project = $client->createProject('...', type: 'order');
// add items, transition...
} catch (Throwable $e) {
if (isset($project['id'])) {
$client->deleteProject((int) $project['id']); // still a draft -> deletable
}
throw $e;
}
examples/submit_and_poll.php shows this inline.