Skip to content

Model client-state Apps* errors as APIError#6856

Draft
agners wants to merge 3 commits into
mainfrom
model-apps-errors-as-api-errors
Draft

Model client-state Apps* errors as APIError#6856
agners wants to merge 3 commits into
mainfrom
model-apps-errors-as-api-errors

Conversation

@agners
Copy link
Copy Markdown
Member

@agners agners commented May 20, 2026

Proposed change

Follow-up on #6739. With HassioError now logged and captured by Sentry in api_process, a handful of Apps* exceptions raised from AppManager.install/update/rebuild and AppModel._validate_availability surfaced as "unexpected" 400s with a noisy log entry and a Sentry event, even though they are all client-state errors (user clicked install on an already-installed add-on, "no update available", local and store versions diverged, system architecture/machine/Home Assistant version incompatible, image-based app cannot be rebuilt, etc.). The dominant offender is SUPERVISOR-1JVV (No update available for app core_mosquitto, ~19k events / ~12k users on 2026.05.0); several siblings in the same wrap_api cluster show the same shape.

Same treatment as #6785 did for BackupMountDownError: map these through properly so the API returns clean, structured 400s.

  • Add modeled APIError subclasses in exceptions.py for the previously raw raises in apps/manager.py: AppAlreadyInstalledError, AppNotFoundError, AppNotInstalledError, AppNotInStoreError, AppNoUpdateAvailableError, AppRebuildVersionChangedError, AppRebuildImageBasedError. Each gets a stable error_key, a message_template, and an addon extra field.
  • Add APIError to AppNotSupportedArchitectureError, AppNotSupportedMachineTypeError and AppNotSupportedHomeAssistantVersionError so they behave the same as the rest of the Apps* APIErrors instead of being treated as unexpected.
  • Use the add-on's display name (app.name from the config) instead of the slug in extra_fields whenever an App/AppModel is available at the raise site, so users see e.g. "Mosquitto broker" rather than "core_mosquitto" in error messages. Slug remains the fallback when there is no app object (install of an unknown slug; update/rebuild of a slug that is not installed).
  • Update raise sites in apps/manager.py and apps/model.py accordingly.

These are all runtime states users hit during normal interaction with the add-ons UI, not Supervisor bugs worth paging on.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality to the supervisor)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • The code has been formatted using Ruff (ruff format supervisor tests)
  • Tests have been added to verify that the new code works.

If API endpoints or add-on configuration are added/changed:

Follow-up on #6739: with HassioError now logged and captured by Sentry
in api_process, a handful of Apps* exceptions raised from
AppManager.install/update/rebuild and AppModel._validate_availability
surfaced as "unexpected" 400s with a noisy log entry and a Sentry
event, even though they are all user/client-state errors (clicked
install on an already-installed add-on, "no update available", local
and store versions diverged, system architecture/machine/HA version
incompatible, etc.). The dominant offender is SUPERVISOR-1JVV
("No update available for app core_mosquitto", ~19k events / ~12k
users), but several siblings show the same shape.

Map these through properly so the API returns clean, structured 400s:

- Add modeled APIError subclasses in exceptions.py for the previously
  raw raises in apps/manager.py: AppAlreadyInstalledError,
  AppNotFoundError, AppNotInstalledError, AppNotInStoreError,
  AppNoUpdateAvailableError, AppRebuildVersionChangedError,
  AppRebuildImageBasedError. Each gets a stable error_key, a
  message_template, and an "addon" extra_field.
- Add APIError to AppNotSupportedArchitectureError,
  AppNotSupportedMachineTypeError and
  AppNotSupportedHomeAssistantVersionError so they behave the same as
  the other Apps* APIErrors instead of being treated as unexpected.
- Pass the app's display name (app.name from the add-on config)
  instead of the slug to extra_fields wherever an App or AppModel is
  available at the raise site, so users see "Mosquitto broker" rather
  than "core_mosquitto" in error messages. Slug is only used as a
  fallback when no app object exists (install of an unknown slug,
  update/rebuild of a slug that is not installed).
- Update raise sites in apps/manager.py and apps/model.py to use the
  new typed exceptions and the addon= keyword.

These are all runtime states users hit during normal interaction with
the apps UI, not Supervisor bugs worth paging on.
@agners agners requested a review from mdegat01 May 20, 2026 14:23
@agners agners added the new-feature A new feature label May 20, 2026
Copy link
Copy Markdown
Contributor

@mdegat01 mdegat01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you're switching to using app name rather then slug in these errors. I get why but I think we should still return the slug for reasons below (both is fine too, just don't skip the slug imo).

Also we should be consistent across all these app errors with whichever we pick, name or slug. Exception of course for ones like AppNotFoundError where all we have is the input slug because its a nonexistent app.

Comment thread supervisor/apps/manager.py Outdated

if slug in self.local:
raise AppsError(f"App {slug} is already installed", _LOGGER.warning)
raise AppAlreadyInstalledError(_LOGGER.warning, addon=self.local[slug].name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the slug not the name here. Or both if you prefer, but if so we should update all the other App errors to be consistent because we use slug in many of these right now (and even within this PR like for AppNotInstalledError below where we use slug but could get the name).

My reasoning is that we can look up name (and any other information about the app) from the slug later if the frontend wants. We cannot look up app info from the name if that's all we have.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed that the Frontend prefixes errors with the App name, which leads to a weird mix of App name and slug in the frontend (see screenshot home-assistant/epics#50 (comment) as an example).

Could we add slug as metadata always and format using the name? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that sounds totally reasonable to me. Want to update all of them like that? Or just do these and then another pr for the other unrelated errors?

@home-assistant
Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant home-assistant Bot marked this pull request as draft May 20, 2026 15:02
agners added 2 commits May 21, 2026 23:17
Address review feedback from #6856: clients still need the slug to look
up additional add-on information (the name is for display only), and we
should be consistent about it across the Apps* errors touched by this
PR.

Every Apps* APIError raised with an App/AppModel available now carries
both `addon` (display name, used by the message_template) and `slug` in
extra_fields. Raise sites in apps/manager.py and apps/model.py pass
both. The two errors raised before an app object exists keep slug-only
extra_fields and use {slug} in their message:

- AppNotFoundError (install of an unknown slug)
- AppNotInstalledError (update/rebuild of a slug not in self.local)

Pre-existing Apps* APIErrors outside the scope of this PR
(AppUnknownError, AppConfigurationInvalidError, AppBootConfigCannot
ChangeError, AppNotRunningError, AppPortConflict, AppNotSupportedWrite
StdinError, AppBuild*) will be migrated in a follow-up.
Address review follow-up on #6856: the addon/slug convention was
enforced only by hand-rolled __init__s, easy to drift on (forget slug,
use a different key, etc.). Promote it to a base class that owns the
shape of extra_fields for all App-related API errors.

- Add AppAPIError(AppsError, APIError). Its __init__ takes
  `app: AppModel | App | AppStore | str` and uniformly populates
  extra_fields with `addon` (display name) and `slug`. Pass a string
  when no app object exists; only `slug` is set in that case. Extra
  per-error fields flow through **extra_fields and merge with the
  defaults.
- Convert the new exceptions added in this PR
  (AppAlreadyInstalledError, AppNotFoundError, AppNotInstalledError,
  AppNotInStoreError, AppNoUpdateAvailableError,
  AppRebuildVersionChangedError) into thin subclasses that only declare
  error_key and message_template -- the __init__ is inherited.
- Migrate the AppNotSupported* errors (architecture, machine type, HA
  version) and AppRebuildImageBasedError to use AppAPIError too;
  their bespoke per-error fields go through **extra_fields. They keep
  inheriting AppNotSupportedError so `except AppNotSupportedError`
  callers (e.g., AppModel._available) still work; MRO routes __init__
  through AppAPIError.
- Update raise sites in apps/manager.py and apps/model.py to pass
  `app=<obj-or-slug>` instead of repeating `addon=...` and `slug=...`.

Pre-existing App* APIErrors outside this PR's scope
(AppUnknownError, AppConfigurationInvalidError,
AppBootConfigCannotChangeError, AppNotRunningError, AppPortConflict,
AppNotSupportedWriteStdinError, AppBuild*) will be migrated to
AppAPIError in a follow-up; the base class is in place for them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants