Error Handling
Minimal-activitypub raises typed exceptions so you can respond precisely to different failure modes.
All exceptions inherit from ActivityPubError, making it easy to catch everything at once or be selective.
Exception Hierarchy
ActivityPubError ← base class for all library exceptions
├── NetworkError ← connection / transport failure
├── ApiError ← generic API error (not commonly raised directly)
├── ServerError ← 5xx responses
└── ClientError ← 4xx responses (catch-all for unmapped status codes)
├── UnauthorizedError 401 — bad or missing access token
├── ForbiddenError 403 — token lacks the required scope
├── NotFoundError 404 — resource does not exist
├── ConflictError 409 — action conflicts with current state
├── GoneError 410 — resource has been deleted
├── UnprocessedError 422 — server rejected the request parameters
└── RatelimitError 429 — rate limit exceeded
Import exceptions directly:
from minimal_activitypub import (
ActivityPubError,
NetworkError,
ServerError,
ClientError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
RatelimitError,
UnprocessedError,
)
Exception Attributes
Every exception exposes the following attributes:
| Attribute | Type | Description |
|---|---|---|
status_code |
int \| None |
HTTP status code (e.g. 401) |
reason_phrase |
str \| None |
HTTP reason phrase (e.g. "Unauthorized") |
message |
str \| None |
Error text extracted from the API response body |
endpoint |
str \| None |
Full URL that was requested (includes query parameters) |
method |
str \| None |
HTTP verb used (e.g. "GET", "POST") |
request_id |
str \| None |
Server-assigned request ID from the X-Request-Id response header, if the server included one |
response_text |
str \| None |
Raw response body, truncated to 500 characters |
occurred_at |
Instant |
Timestamp when the exception was raised (whenever.Instant) |
NetworkError is raised via exception chaining (raise NetworkError from transport_error) so status_code,
endpoint, and related attributes will be None; use __cause__ to access the underlying httpx error.
Note on request body parameters: Request body content (status text, media descriptions, etc.) is intentionally not captured in exceptions to avoid logging sensitive or user-authored content. Query parameters are accessible via
endpoint.
Common Patterns
Catch specific exceptions
from minimal_activitypub import RatelimitError, UnauthorizedError, NetworkError
from minimal_activitypub.client_2_server import ActivityPub
from httpx import AsyncClient
async def post_safely(ap: ActivityPub, text: str) -> dict | None:
try:
return await ap.post_status(text)
except UnauthorizedError:
print("Access token is invalid or expired — re-authenticate.")
except RatelimitError as e:
print(f"Rate limited at {e.endpoint}. Reset at: {ap.ratelimit_reset}")
except NetworkError as e:
print(f"Connection failed: {e.__cause__}")
return None
Inspect error context
from minimal_activitypub import ActivityPubError
async def call_with_logging(ap: ActivityPub) -> list:
try:
return await ap.get_home_timeline()
except ActivityPubError as e:
print(f"[{e.occurred_at}] {e.method} {e.endpoint} → {e.status_code} {e.reason_phrase}")
if e.request_id:
print(f" Server request ID: {e.request_id} (quote this when reporting to instance admin)")
if e.message:
print(f" API message: {e.message}")
raise
Handle 422 Unprocessable Entity
Mastodon returns 422 when request parameters are rejected — for example posting an empty status or
providing an invalid visibility value.
from minimal_activitypub import UnprocessedError
try:
await ap.post_status("") # empty status
except UnprocessedError as e:
print(f"Rejected: {e.message}") # e.g. "Validation failed: Text can't be blank"
Handle rate limiting
Rate limit metadata is tracked on the ActivityPub instance after every response. When
RatelimitError is raised, ap.ratelimit_reset tells you when the limit resets.
import asyncio
from whenever import Instant
from minimal_activitypub import RatelimitError
async def post_with_backoff(ap: ActivityPub, text: str) -> dict:
try:
return await ap.post_status(text)
except RatelimitError:
reset = ap.ratelimit_reset
if reset is not None:
wait = max(0, (reset - Instant.now()).in_seconds())
print(f"Rate limited. Waiting {wait:.0f}s until reset.")
await asyncio.sleep(wait + 1)
return await ap.post_status(text)
Catch all library errors
from minimal_activitypub import ActivityPubError
try:
timeline = await ap.get_public_timeline()
except ActivityPubError as e:
print(f"Error: {e}") # str(e) → "404 Not Found at https://instance/api/v1/... — Record not found"
Common Error Scenarios
401 Unauthorized
Raised when the access token is missing, expired, or revoked.
from minimal_activitypub import UnauthorizedError
try:
await ap.get_home_timeline()
except UnauthorizedError:
# Re-authenticate and obtain a new token
new_token = await get_fresh_token()
403 Forbidden
Raised when the token is valid but lacks the required OAuth scope (e.g. trying to write:statuses
with a read-only token).
404 Not Found
Raised when the requested resource does not exist — for example, deleting a status that has already been deleted, or looking up a non-existent account ID.
410 Gone
Raised when a resource existed but has been permanently removed. Unlike 404, this signals that retrying will never succeed.
422 Unprocessable Entity
Raised when the server rejects the request parameters. The message attribute contains the
server's explanation (e.g. "Validation failed: Visibility is not included in the list").
429 Rate Limited
Raised when the request rate limit is exceeded. Check ap.ratelimit_remaining proactively
or handle RatelimitError reactively. Takahe and Pleroma instances may not return rate limit
headers; the library defaults to a 5-minute reset window in that case.
5xx Server Error
ServerError covers all 5xx responses. These indicate a problem on the instance's side.
The response_text attribute contains the raw response body (truncated to 500 characters)
which may include a stack trace or error page useful for reporting to instance admins.
Network errors
NetworkError wraps low-level httpx transport errors (connection refused, DNS failure,
timeout). Access the original exception via __cause__:
from minimal_activitypub import NetworkError
try:
await ap.verify_credentials()
except NetworkError as e:
original = e.__cause__
print(f"Transport error: {type(original).__name__}: {original}")