Error Handling¶
longwei 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 longwei:
from longwei 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 |
datetime |
Timestamp (UTC-aware) when the exception was raised |
RatelimitError also exposes two additional attributes:
| Attribute | Type | Description |
|---|---|---|
ratelimit_reset |
datetime \| None |
When the rate-limit window resets; None if the server did not include the header |
ratelimit_limit |
int \| None |
Total requests allowed per window; None if the server did not include the header |
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 longwei import APClient, RatelimitError, UnauthorizedError, NetworkError
from longwei import Status
async def post_safely(ap: APClient, text: str) -> "Status | 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: {e.ratelimit_reset}")
except NetworkError as e:
print(f"Connection failed: {e.__cause__}")
return None
Inspect error context¶
from longwei import APClient, ActivityPubError
async def call_with_logging(ap: APClient) -> 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 longwei 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 APClient instance after every response, and is also
embedded directly in the RatelimitError exception for convenience.
import asyncio
from datetime import datetime, timezone
from longwei import RatelimitError
async def post_with_backoff(ap: "APClient", text: str) -> "Status":
try:
return await ap.post_status(text)
except RatelimitError as e:
reset = e.ratelimit_reset
if reset is not None:
now = datetime.now(tz=timezone.utc)
wait = max(0, (reset - now).total_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 longwei 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 longwei 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. The exception carries ratelimit_reset and
ratelimit_limit directly so you do not need a reference to the APClient instance to
determine when to retry. Pleroma instances may not return rate limit headers; in
those cases ratelimit_reset and ratelimit_limit will be None on the exception (though
ap.ratelimit_reset is still set to a synthetic 5-minute window).
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__: