Skip to content

Error Handling

jsem-nerad edited this page Sep 1, 2026 · 4 revisions

Error Handling

The hierarchy

Every exception the library raises derives from StravaError, so one except catches all of it:

StravaError
├── StravaAPIError            .code, .status_code, .payload
│   ├── AuthenticationError
│   │   └── NotLoggedInError
│   └── InsufficientBalanceError
├── MealNotFoundError         .meal_id
├── MealNotOrderableError     .meal_id, .reason, .action
├── DuplicateMealError
├── MenuNotFetchedError
└── ProfileError
    ├── ProfileNotFoundError  .name
    ├── KeyringUnavailableError
    └── EncryptionUnavailableError
from strava_cz import StravaError

try:
    strava.menu.order_meals(5)
except StravaError as exc:
    print(f"Something went wrong: {exc}")

The library never lets a raw httpx or json exception escape.

The exceptions

StravaAPIError

The API refused a request or answered with something unusable. It carries what the canteen actually said:

from strava_cz import StravaAPIError

try:
    strava.menu.fetch()
except StravaAPIError as exc:
    print(exc.message)      # the canteen's own wording
    print(exc.code)         # the API's error number, or None
    print(exc.status_code)  # the HTTP status, or None
    print(exc.payload)      # the decoded body, for debugging

Strava.cz signals failure with a non-standard HTTP 555 and a body shaped {"state": "error", "number": ..., "message": ...}. Known numbers are mapped onto specific exceptions; anything else arrives as a StravaAPIError with code set.

AuthenticationError

Login rejected, or the session is no longer valid.

from strava_cz import AuthenticationError

try:
    strava = StravaCZ("user", "wrong-password", "3753")
except AuthenticationError as exc:
    print(exc.code)   # 20

Also raised when logging in twice on one client.

NotLoggedInError

An operation that needs a session was attempted without one. A subclass of AuthenticationError, so catching that catches this too.

from strava_cz import NotLoggedInError, StravaCZ

try:
    StravaCZ().menu.fetch()
except NotLoggedInError:
    print("log in first")

InsufficientBalanceError

The account cannot cover the order. The transaction is rolled back, so nothing is partly ordered.

from strava_cz import InsufficientBalanceError

try:
    strava.menu.order_meals(5)
except InsufficientBalanceError as exc:
    print(f"{exc.message} - you have {strava.user.balance} {strava.user.currency}")

MealNotFoundError

No meal with that id is in the fetched menu — usually a stale id from before the canteen republished the menu.

from strava_cz import MealNotFoundError

try:
    strava.menu.order_meals(999999)
except MealNotFoundError as exc:
    print(exc.meal_id)
    strava.menu.fetch()   # ids change with the menu; get fresh ones

MealNotOrderableError

The canteen does not allow the change. .reason is a Restriction explaining why, and .action is "order" or "cancel".

from strava_cz import MealNotOrderableError, Restriction

try:
    strava.menu.order_meals(85)
except MealNotOrderableError as exc:
    if exc.reason is Restriction.CLOSED:
        print("too late for this day")
    elif exc.reason is Restriction.UNAVAILABLE:
        print("this cannot be ordered on its own")
    else:
        print(exc.reason.description)

Checking first avoids the exception entirely:

meal = strava.menu.get_by_id(85)
if meal and meal.can_order:
    strava.menu.order_meals(meal.id)

DuplicateMealError

Only raised with strict_duplicates=True, when two chosen meals compete for the same slot. See Ordering Meals.

ProfileError and friends

Raised by the profile store rather than by the canteen — see Profiles. ProfileNotFoundError means no such profile (or no default is set). KeyringUnavailableError means this machine has no usable credential store, and EncryptionUnavailableError that cryptography is not installed to encrypt with instead; both messages list the ways out. Plain ProfileError covers the rest: a bad profile name, a corrupt store file, a wrong passphrase, or a password that cannot be found anywhere.

from strava_cz import (
    KeyringUnavailableError,
    ProfileNotFoundError,
    StravaCZ,
)

try:
    strava = StravaCZ.from_profile("skola")
except ProfileNotFoundError:
    print("Run: strava-cz profile add skola --username ... --canteen ...")
except KeyringUnavailableError as exc:
    print(exc)   # says what to use instead on a headless machine

On the command line all three exit with code 6.

MenuNotFetchedError

The menu was used before fetch().

from strava_cz import MenuNotFetchedError

try:
    days = strava.menu.get_days()
except MenuNotFetchedError:
    days = strava.menu.fetch().get_days()

# or just ask
if not strava.menu.fetched:
    strava.menu.fetch()

Network problems

Timeouts and connection failures arrive as StravaAPIError, with the underlying httpx error kept as __cause__:

try:
    strava.menu.fetch()
except StravaAPIError as exc:
    print(exc)               # Request to 'objednavky' timed out
    print(exc.__cause__)     # the original httpx exception

Every request carries a timeout (15s, 10s to connect by default), and connections that never got established are retried twice. Requests that reached the server are never retried, so an order cannot be placed twice by a retry.

A maintenance page or a gateway error — anything that is not JSON — also becomes a StravaAPIError, with a snippet of the body in the message:

StravaAPIError: 'objednavky' returned 502 with a non-JSON body: '<html>Bad Gateway</html>'

Recovering from a failed transaction

By default a failed order rolls back and re-raises, so the canteen keeps the state it had. Your local menu is refreshed as part of that, so you can simply look again:

from strava_cz import StravaError

try:
    strava.menu.order_meals(5, 9)
except StravaError as exc:
    print(f"Nothing was ordered: {exc}")
    for meal in strava.menu.get_meals(orderable=True):
        print(f"  still available: [{meal.id}] {meal.name}")

To get partial success instead of an exception, use continue_on_error=True and read the OrderResult.

Warnings

Skipped duplicates are reported through the warnings module rather than an exception:

import warnings

with warnings.catch_warnings():
    warnings.simplefilter("error")   # turn them into exceptions
    strava.menu.order_meals(9, 10)

A robust script

import os
import sys

from strava_cz import (
    AuthenticationError,
    InsufficientBalanceError,
    MealType,
    StravaCZ,
    StravaError,
)

try:
    with StravaCZ(
        os.environ["STRAVA_USERNAME"],
        os.environ["STRAVA_PASSWORD"],
        os.environ["STRAVA_CANTEEN"],
    ) as strava:
        strava.menu.fetch()

        wanted = [
            day.orderable[0].id
            for day in strava.menu.get_days()
            if day.orderable and not day.ordered
        ]
        result = strava.menu.order_meals(*wanted, continue_on_error=True)

        print(f"Ordered {len(result.changed)} meals, {len(result.failed)} failed")
        for meal_id, error in result.failed:
            print(f"  {meal_id}: {error}", file=sys.stderr)

except AuthenticationError:
    sys.exit("Check your credentials")
except InsufficientBalanceError as exc:
    sys.exit(f"Top up your account: {exc.message}")
except StravaError as exc:
    sys.exit(f"Strava.cz problem: {exc}")

Clone this wiki locally