Skip to content

Improve exception hierarchy with structured method/path/status attributes#2085

Open
mik-laj wants to merge 5 commits into
frenck:mainfrom
mik-laj:excpetions
Open

Improve exception hierarchy with structured method/path/status attributes#2085
mik-laj wants to merge 5 commits into
frenck:mainfrom
mik-laj:excpetions

Conversation

@mik-laj

@mik-laj mik-laj commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR improves the exception hierarchy in the WLED library to allow consumers (e.g. Home Assistant) to distinguish which endpoint failed without fragile string matching on exception messages.

Motivation

Home Assistant's WLED integration was doing (PR: home-assistant/core#171646 ):

except (WLEDInvalidResponseError, WLEDEmptyResponseError) as er:
    if "presets" in str(er):
        errors["base"] = "invalid_response_presets"
    else:
        errors["base"] = "invalid_response"

A reviewer correctly noted that parsing exception message strings is fragile. This PR moves that distinction into the library.


Exception hierarchy

Before

WLEDError
├── WLEDEmptyResponseError   ← inherited from bare Exception (bug)
├── WLEDInvalidResponseError
├── WLEDConnectionError
│   ├── WLEDConnectionTimeoutError
│   └── WLEDConnectionClosedError
├── WLEDUnsupportedVersionError
└── WLEDUpgradeError

After

WLEDError
├── WLEDConnectionError
│   ├── WLEDConnectionTimeoutError
│   └── WLEDConnectionClosedError
├── WLEDStatusError              ← NEW: HTTP 4xx/5xx responses
├── WLEDResponseError            ← NEW: base for 2xx responses with unusable body
│   ├── WLEDEmptyResponseError   ← moved under WLEDResponseError
│   └── WLEDInvalidResponseError ← moved under WLEDResponseError
├── WLEDUnsupportedVersionError
└── WLEDUpgradeError

Why this separation?

WLEDStatusError and WLEDResponseError represent fundamentally different failure modes:

  • WLEDStatusError — the device explicitly said "no" (returned a 4xx/5xx HTTP status). This is an HTTP-level error. It carries .status (int) and .body so consumers can inspect what the device reported.
  • WLEDResponseError — the device said "ok" (2xx), but the body is unusable. There are two sub-cases: empty body (WLEDEmptyResponseError) and unparseable body (WLEDInvalidResponseError). These can be caught together since HA treats them identically — both mean "response body can't be used."

There is no practical use case for catching all three in one handler, so grouping them under a single base would be misleading. Keeping WLEDStatusError as a direct child of WLEDError makes the distinction explicit.


Changes in detail

src/wled/exceptions.py

  • WLEDEmptyResponseError fixed to inherit from WLEDError (was incorrectly inheriting from bare Exception)
  • New WLEDResponseError(WLEDError) base class with .method and .path keyword-only attributes; WLEDEmptyResponseError and WLEDInvalidResponseError now inherit from it
  • New WLEDStatusError(WLEDError) for HTTP 4xx/5xx responses, with .method, .path, .status, .body attributes
  • All custom __init__ use *args forwarded to super().__init__(*args) to preserve the full backward-compatible positional-argument contract of Exception

src/wled/wled.py

  • request() now raises WLEDStatusError (with method, path, status, body) instead of bare WLEDError(response.status, body) for 4xx/5xx responses
  • WLEDInvalidResponseError and WLEDEmptyResponseError raised with method= and path= so callers can inspect which endpoint failed
  • Fixed accidental tuple construction in error message strings (trailing comma bug in update() and listen())
  • Updated docstrings for update() and listen() to document additional raised exceptions

src/wled/__init__.py

  • Exports WLEDResponseError and WLEDStatusError

tests/test_wled.py

  • Assertions on .method and .path added to preset-related error tests

After this PR, HA can replace

except (WLEDInvalidResponseError, WLEDEmptyResponseError) as er:
    if "presets" in str(er):
        errors["base"] = "invalid_response_presets"
    else:
        errors["base"] = "invalid_response"

with:

except (WLEDInvalidResponseError, WLEDEmptyResponseError) as er:
    if er.path == "/presets.json":
        errors["base"] = "invalid_response_presets"
    else:
        errors["base"] = "invalid_response"

Test plan

  • pytest tests/ — all 222 tests pass
  • Backward compatible: existing code that catches WLEDInvalidResponseError, WLEDEmptyResponseError, or WLEDError continues to work
  • WLEDEmptyResponseError retry backoff in update() still works (inherits from WLEDResponseErrorWLEDError)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added WLEDResponseError and WLEDStatusError exception types for more granular error handling.
    • Exception instances now capture HTTP method, path, status code, and response body for improved debugging.
  • Tests

    • Added comprehensive test coverage for new exception types and error scenarios.

WLEDEmptyResponseError and WLEDInvalidResponseError now expose `method`
and `path` attributes, allowing consumers like Home Assistant to
distinguish which endpoint failed (e.g. /presets.json vs /json) without
fragile string matching on the exception message.

Also fixes WLEDEmptyResponseError inheriting from bare Exception instead
of WLEDError, and corrects accidental tuple construction in error
message strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 10, 2026 19:35
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mik-laj, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 14 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf6bf706-305b-4222-85d7-305425526b41

📥 Commits

Reviewing files that changed from the base of the PR and between 3388359 and 34af9d8.

📒 Files selected for processing (1)
  • src/wled/wled.py
📝 Walkthrough

Walkthrough

Introduce WLEDResponseError and WLEDStatusError; make WLEDEmptyResponseError and WLEDInvalidResponseError inherit and store request metadata; propagate method/path/status/body through WLED.request(), update/listen empty-response branches, export the new exception types, and update tests to assert the metadata.

Changes

Request context enrichment for WLED exceptions

Layer / File(s) Summary
Exception class contract
src/wled/exceptions.py
Add WLEDResponseError and WLEDStatusError with init storing method/path (status/body for WLEDStatusError); redefine WLEDEmptyResponseError and WLEDInvalidResponseError to inherit from WLEDResponseError.
HTTP request helper error enrichment
src/wled/wled.py
WLED.request() raises WLEDInvalidResponseError with method/path on JSON/UTF-8 decode failures and raises WLEDStatusError(method, path, status, body) for 4xx/5xx JSON error bodies.
WebSocket listen and update handling
src/wled/wled.py
WLED.listen() and WLED.update() document new exceptions; empty /presets.json and empty /json branches now raise WLEDEmptyResponseError(method="GET", path="...").
Package exports
src/wled/__init__.py
Import and re-export WLEDResponseError and WLEDStatusError via __all__ to make them public.
Exception metadata test validation
tests/test_wled.py
New parametrized test for WLEDStatusError; updated tests (test_update_corrupt_presets_response, test_update_empty_presets_response, test_listen_preset_change_empty_response) capture exceptions and assert method == "GET" and path == "/presets.json".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble through tracebacks with a hop and a wink,

Each exception now shows method and path in a blink,
Status and body tucked under my paw,
Hunting down bugs with a precise little gnaw,
Hooray for metadata — carrots for the log!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely captures the main change: introducing improved exception handling with structured attributes (method, path, status) across the exception hierarchy.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mik-laj mik-laj added the enhancement Enhancement of the code, not introducing new features. label Jun 10, 2026
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.54%. Comparing base (3e87d76) to head (34af9d8).
⚠️ Report is 617 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2085       +/-   ##
===========================================
+ Coverage   58.61%   97.54%   +38.93%     
===========================================
  Files           6        8        +2     
  Lines         662     1141      +479     
  Branches      143      113       -30     
===========================================
+ Hits          388     1113      +725     
+ Misses        270       18      -252     
- Partials        4       10        +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

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.

Pull request overview

This PR improves debuggability and downstream error handling by enriching response-related exceptions with the HTTP method and request path, allowing consumers to identify the failing endpoint without parsing exception strings.

Changes:

  • Add keyword-only .method and .path attributes to WLEDEmptyResponseError and WLEDInvalidResponseError.
  • Fix accidental tuple construction in empty-response error message composition and attach method/path when raising these exceptions.
  • Extend preset-related tests to assert .method/.path values and update listen()/update() docstrings to include the additional raised exception type.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/wled/exceptions.py Adds method/path attributes and adjusts exception inheritance.
src/wled/wled.py Raises enriched exceptions (and fixes tuple-message bug) in preset and request error paths; updates docstrings.
tests/test_wled.py Adds assertions validating .method/.path on preset-related error cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/wled/exceptions.py
Comment thread src/wled/exceptions.py
Raised in code review: requiring `message` breaks `raise WLEDEmptyResponseError`
and no-arg construction, which worked before this change. Default to "" to
keep backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mik-laj mik-laj marked this pull request as ready for review June 10, 2026 19:44
Copilot AI review requested due to automatic review settings June 10, 2026 19:44

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/wled/exceptions.py
Comment thread src/wled/exceptions.py
Introduces a cleaner separation between HTTP-level and response-body errors:

- WLEDResponseError: new base for 2xx responses with unusable bodies
  - WLEDEmptyResponseError (was a direct WLEDError subclass)
  - WLEDInvalidResponseError (was a direct WLEDError subclass)
- WLEDStatusError: HTTP 4xx/5xx errors, direct child of WLEDError
- WLEDResponseError/WLEDStatusError expose .method and .path attributes
  using *args to preserve full backward compatibility with Exception contract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mik-laj mik-laj marked this pull request as draft June 10, 2026 20:21
@mik-laj mik-laj changed the title Add method and path attributes to response error exceptions Improve exception hierarchy with structured method/path/status attributes Jun 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/wled/wled.py (1)

245-247: ⚡ Quick win

WLEDStatusError raised without a message may hinder debugging.

When raising WLEDStatusError on lines 245-247 and 258-263, no message is passed. While the exception stores method, path, status, and body, calling str(exception) returns an empty string, making logs and tracebacks less informative.

♻️ Suggested improvement
-                raise WLEDStatusError(
-                    method=method, path=uri, status=response.status, body=error_body
-                )
+                msg = f"HTTP {response.status} error from request: {method} {uri}"
+                raise WLEDStatusError(
+                    msg, method=method, path=uri, status=response.status, body=error_body
+                )

And similarly for lines 258-263:

-                raise WLEDStatusError(
-                    method=method,
-                    path=uri,
-                    status=response.status,
-                    body={"message": message},
-                )
+                msg = f"HTTP {response.status} error from request: {method} {uri}"
+                raise WLEDStatusError(
+                    msg,
+                    method=method,
+                    path=uri,
+                    status=response.status,
+                    body={"message": message},
+                )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wled/wled.py` around lines 245 - 247, The WLEDStatusError is being raised
without a descriptive message in the raise statements that build it with method,
path (uri), status (response.status) and body (error_body); update those raise
sites (the WLEDStatusError constructor calls) to include a formatted
human-readable message (e.g., f"HTTP {response.status} {method} {uri}:
{shortened/error summary from error_body}") so that str(exception) returns
useful info while still preserving method, path, status and body on the
exception object; apply the same change to both raise locations that currently
pass only keyword fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/wled/wled.py`:
- Around line 245-247: The WLEDStatusError is being raised without a descriptive
message in the raise statements that build it with method, path (uri), status
(response.status) and body (error_body); update those raise sites (the
WLEDStatusError constructor calls) to include a formatted human-readable message
(e.g., f"HTTP {response.status} {method} {uri}: {shortened/error summary from
error_body}") so that str(exception) returns useful info while still preserving
method, path, status and body on the exception object; apply the same change to
both raise locations that currently pass only keyword fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d717b15-3874-4204-8b41-178f9b8870af

📥 Commits

Reviewing files that changed from the base of the PR and between a1e8dae and 04a9400.

📒 Files selected for processing (3)
  • src/wled/__init__.py
  • src/wled/exceptions.py
  • src/wled/wled.py

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread src/wled/wled.py
Comment thread src/wled/wled.py
Comment thread src/wled/wled.py
Pass (status, body) as positional args when raising WLEDStatusError so
that exc.args and str(exc) remain identical to the previous
WLEDError(status, body) behavior.

Add test asserting WLEDStatusError with .method, .path, .status, .body
and .args to lock in the structured error contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mik-laj mik-laj marked this pull request as ready for review June 10, 2026 20:35
Copilot AI review requested due to automatic review settings June 10, 2026 20:35

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread src/wled/exceptions.py
Comment thread src/wled/wled.py
Comment thread src/wled/wled.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Enhancement of the code, not introducing new features.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants