Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error handling

Read the exception message first. A missing credential needs a setup change; a rejected filter needs a schema check. Repeating the same request will not necessarily fix either problem.

Client errors inherit from pyaviso.AvisoError. Python input errors can also raise TypeError or ValueError, and a missing environment variable accessed through os.environ[...] raises KeyError. AvisoError does not catch these or errors in your own analysis.

Catching everything

Use the quickstart setup, including AVISO_BASE_URL and credentials for pyaviso.Env(). For an anonymous server, pass auth=pyaviso.Anonymous() instead.

This listener uses the small mars schema. It has a required class choice (od or rd) and optional whole-number step filter. Providers must supply both identifiers; the payload is optional. Listening requires receiving permission, not publishing permission.

Save this as listen_errors.py and run python listen_errors.py:

import os

import pyaviso

try:
    client = pyaviso.AvisoClient(
        base_url=os.environ["AVISO_BASE_URL"], auth=pyaviso.Env()
    )
    with client.listen("mars", filter={"class": "od"}) as notifications:
        for notification in notifications:
            print(notification)
except pyaviso.AvisoError as error:
    print("Listening failed:", error)
    raise
except KeyboardInterrupt:
    print("Stopped listening")

It prints matching notifications as indented CloudEvent JSON until you press Ctrl+C. Setup is inside try because Env() can fail before listening starts. The with block closes the iterator even if your loop raises an exception. The error branch reports the failure and reraises it, so a failed job does not look like a successful run.

HttpError exposes the server’s response

HttpError carries the integer status, string body and optional request_id. Keep the request ID when asking your operator to find the request in server logs. The response body explains what the server rejected.

For providers, here is an intentionally invalid publish. Save it as bad_publish.py and run python bad_publish.py with publishing credentials and the same environment setup:

import json
import os

import pyaviso

client = pyaviso.AvisoClient(
    base_url=os.environ["AVISO_BASE_URL"], auth=pyaviso.Env()
)
try:
    client.notify(event_type="mars", identifier={"class": "od"})
except pyaviso.HttpError as error:
    print("status:", error.status)
    print(json.dumps(json.loads(error.body), indent=2, sort_keys=True))

Captured output with the small mars schema (your request ID will differ):

status: 400
{
  "code": "INVALID_NOTIFICATION_REQUEST",
  "details": "Required field 'step' missing for notify operation",
  "error": "Invalid Notification Request",
  "message": "Required field 'step' missing for notify operation",
  "request_id": "daf5f6d6-6f40-42db-a1e5-8b22df279b07"
}

The server rejects this with HTTP 400 because step is missing. It is optional in listener filters, but required when publishing. This example decodes the Aviso server’s JSON error body; a proxy or another server can return plain text, so general error handlers should not assume every body is JSON.

When errors propagate

  • Setup can fail when constructing a provider, client, request or state store.
  • HTTP methods such as notify and schema raise when the request fails. notify_many returns per-notification results; inspect every result. Invalid batch input can raise before any request is sent.
  • Listening can fail when opening the iterator or during iteration. After a terminal stream error is delivered, subsequent iteration is exhausted.
  • A required trigger failure stops the listener before that notification reaches your loop. Earlier trigger effects are not undone.

Connection losses and retryable server responses normally cause listeners to reconnect. One-shot publishes are not automatically retried after transport failure: the server may already have stored the notification. An auth provider is asked to refresh after HTTP 401, followed by one retry if refresh succeeds. If authentication is still rejected while opening a watch, the listener raises AuthError. A one-shot HTTP request instead exposes the final 401 as HttpError. See Authentication.

HistoryGapError carries a reason

A history gap ends the iterator. Inspect error.reason:

ReasonMeaningUseful fields
replay_limit_reachedThe server capped the requested replaymax_allowed
sequence_jumpThe protocol reported an unexpected sequence boundaryexpected, observed

Do not treat a failed replay as complete. Check server retention and replay limits with the operator, then choose a starting point appropriate to your work. Starting live skips historical work. Also, start_from=None uses an existing saved cursor when one is configured; it does not override it. See choosing a starting position.

TriggerError carries a kind and a sub-kind

trigger_kind identifies the action: echo, log, command, webhook, teams, post, function or unknown. error_kind describes the failure:

Error kindUseful fields
commandexit_code, stderr_tail
webhookstatus, body_tail
timeouttimeout_seconds
templatecontext, field, template_kind
iopath for a log trigger; read the exception message
webhook_buildreason
raisedA Trigger.function raised; the original exception is __cause__
encode, unknownRead the exception message

Fields that do not apply are None. Required triggers stop the watch when their retry policy is exhausted or fail-fast applies. Optional triggers warn and let processing continue. Only make an action optional when continuing without it is acceptable. See trigger retry settings.

The failing notification does not advance the pending cursor. An earlier notification may already have been checkpointed. Restarting does not guarantee recovery of unfinished work; see State and resume.

Catching is not the same as recovering

Before retrying a publish, determine whether it may already have succeeded. For a malformed stream event or protocol error, reconnecting to the same input may reproduce the failure. Preserve the error and ask the operator to investigate instead of silently moving past it.

Closing a listener cancels and waits for its background task. It does not certify that your analysis finished. Ctrl+C handling while waiting for input does not set a deadline for interrupting Python work or waiting for cleanup.

Hierarchy

All of these inherit directly from AvisoError:

ExceptionWhat to check
AuthErrorCredential source or watch authentication rejected after refresh
ConfigErrorClient settings, auth file or request options
HttpErrorServer status and response body
TransportErrorConnection or response-transfer failure
DecodeErrorUnexpected response format
MalformedEventErrorInvalid CloudEvent identity
HistoryGapErrorReplay limit or sequence boundary
StreamProtocolErrorFatal streaming protocol condition
StateStoreErrorLocal state path, permissions or contents
TriggerErrorFailed required action

With AsyncAvisoClient

The same exceptions can arise from an awaited method or async for iteration. Use async with on the iterator to await aclose() on exit. With asyncio.run(), catch KeyboardInterrupt outside the run call, as in the async listener. A TaskGroup can wrap task failures in an exception group.