FailureClassifier
Reading why the platform refused a call — the seven kinds the core acts on, what each makes the core do, and how to map a platform's error vocabulary onto them.
Subscriby\Connector\Contracts\Ports\FailureClassifier. Required of every connector.
Each platform has its own error vocabulary and its own habit of refusing inside a successful HTTP response. The connector reads both shapes; the core decides once, from the kind, whether to retry, wait, alert the creator or give up. Every send, grant, revoke and probe your other ports perform ends in a DeliveryFailure built here when it did not succeed.
The method
public function classify(mixed $responseOrThrowable): DeliveryFailure;The argument is whatever your platform client gave you: a decoded response body, a response object, or the exception the call raised. Return a DeliveryFailure:
| Field | Meaning |
|---|---|
kind | One of the seven DeliveryFailureKinds below. |
detail | The platform's own words, for logs and the connector doctor. Never a credential. |
retryAfterSeconds | For RateLimited: how long the platform asked you to wait. |
code | The platform's error code, when it has one, for your own tests and dashboards. |
The seven kinds
| Kind | Means | Retryable | Creator-actionable |
|---|---|---|---|
Unreachable | The account exists but cannot be written to: it blocked the bot, closed its messages, left the platform. | — | — |
NotPermitted | The installation lacks a right it needs in a place: not an administrator, a missing permission. | — | ✅ |
TargetMissing | The place, message or account no longer exists. | — | — |
RateLimited | The platform asked for a pause; carry its wait in retryAfterSeconds. | ✅ | — |
Configuration | The installation itself is wrong: a revoked token, a deleted bot, a call the manifest says the platform cannot make. | — | ✅ |
Transient | The platform or the network hiccuped. | ✅ | — |
Other | Nothing above fits. Make it rare. | — | — |
The two columns are the enum's own methods, DeliveryFailureKind::isRetryable() and isCreatorActionable(), and they are what the core reads: a retryable failure is tried again (after retryAfterSeconds for a rate limit), a creator-actionable one becomes a health verdict the creator is shown with the connector's own remedy (RecoverySupport::healthReasonText()), and everything else is recorded as failed for that call. On a recovery probe of an account, TargetMissing has one more meaning: the account is gone, which is the incident the Disaster Recovery Program exists for, while any other kind is left alone.
Mapping a platform
Write the mapping as a table in your connector, from the platform's codes and phrases to the kinds, and test it row by row. Three habits keep it honest:
- Read the body, not only the status. Telegram answers
200 OKwith"ok": falseand a description; Discord answers403for four different situations only the JSON code tells apart. - Never let a network exception fall through as
Other. A timeout, a connection refused, a DNS failure isTransient. - Classify what your own client throws. A validation exception from the client library about a message the platform would reject is
Configuration, because retrying cannot help.
How Telegram does it
The sentence reading lives in the connector's TelegramRefusal, which the pre-SDK code already had: bot was blocked by the user and user is deactivated are Unreachable, chat not found and message to delete not found are TargetMissing, not enough rights is NotPermitted, Too Many Requests is RateLimited with Telegram's retry_after, Unauthorized (a revoked token) is Configuration. The port adds the two cases the refusal reader had no word for: a bot that lacks rights in a chat, and a network that never answered.
What the kit checks
failures.classifies_anything: classify() returns a DeliveryFailure for an arbitrary throwable and for an arbitrary string, and does not throw itself. A classifier that only understands its own client's exceptions fails here.
One classifier, every port
Your Messenger, AccessController, SupportRelay and RecoverySupport should all build their failures through this class, so the core sees one vocabulary and your tests cover one mapping. The core also calls it directly when a port throws instead of returning.
How is this guide?
InboundGateway
Turning what the platform sends into authenticated, deduplicated events — the four methods, the InboundEnvelope, and exactly what the core's inbound gate does before and after your route runs.
TextRenderer
Turning the core's canonical HTML into what one platform accepts — the eight tags, what a renderer must keep, and how HTML, Markdown and plain-text platforms each do it.