Skip to content

Implement InvalidRequestError with field-level validation details#26

Merged
codebestia merged 2 commits into
ShadeProtocol:mainfrom
samueloyibodevv:fix/17-implement-invalid-request-error
Jun 28, 2026
Merged

Implement InvalidRequestError with field-level validation details#26
codebestia merged 2 commits into
ShadeProtocol:mainfrom
samueloyibodevv:fix/17-implement-invalid-request-error

Conversation

@samueloyibodevv

@samueloyibodevv samueloyibodevv commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR implements InvalidRequestError in the Shade Python SDK so developers get clear, field-level feedback when the API rejects malformed or invalid request parameters. It parses the API error response format, exposes the offending field and all field errors, and raises the exception on HTTP 400/422 responses.

Related Issue

Closes #17

Changes

⚠️ Invalid Request Error Handling

  • [MODIFY] src/shade/errors.py
  • Expanded InvalidRequestError(ShadeError) with param: Optional[str] and field_errors: dict.
  • Added parsing for API error responses such as {"error": {"code": "invalid_param", "param": "amount", "message": "..."}}.
  • Added InvalidRequestError.from_response() to construct the exception from raw 400/422 response bodies.
  • Added raise_for_invalid_request() to raise InvalidRequestError on HTTP 400 and 422 responses.
  • Updated __str__() so the offending parameter name is included in the error message when available.

🧪 Tests

  • [MODIFY] tests/test_errors.py
  • Added tests for parsing param from API error responses.
  • Added tests for parsing field_errors from API error responses.
  • Added tests verifying str(error) includes the param name.
  • Added tests verifying HTTP 400 and 422 responses raise InvalidRequestError.
  • Added tests verifying explicit constructor arguments override parsed body values.
  • Added tests verifying non-400/422 status codes do not raise.

Verification Results

pytest tests/test_errors.py tests/test_gateway.py -v ✅ passed (13/13)
Acceptance Criteria Status
A 400 response raises InvalidRequestError
error.param names the offending field when the API provides one
error.field_errors contains all field-level errors from the API response
str(error) includes the param name in the message

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling for invalid requests so API responses now surface clearer messages, status codes, and field-specific validation details.
    • Error output now includes the affected parameter when available, making failures easier to understand.
    • Added support for handling both common invalid-request status codes consistently.

Add param and field_errors parsing from API error responses and raise
InvalidRequestError on HTTP 400/422 so developers can identify bad params.
@codebestia

Copy link
Copy Markdown
Contributor

Hello @samueloyibodevv
Please resolve the conflicts.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

InvalidRequestError is extended with param and field_errors attributes populated by parsing 400/422 response bodies. A from_response classmethod and a raise_for_invalid_request function are added. Internal parsing is refactored with a typed _parse_body and a new _parse_error_response helper. Tests cover all new behaviors.

InvalidRequestError structured validation

Layer / File(s) Summary
Constants, parsing helpers, and raise_for_invalid_request
src/shade/errors.py
Adds INVALID_REQUEST_STATUS_CODES = (400, 422), refactors _parse_body to dict[str, Any] return type, adds _parse_error_response to extract message/param/field_errors from nested API error objects, and introduces raise_for_invalid_request.
InvalidRequestError constructor, __str__, and from_response
src/shade/errors.py
Constructor accepts optional param and field_errors, falling back to parsed response body values. __str__ conditionally appends param. New from_response classmethod constructs the exception from a raw response body.
Tests
tests/test_errors.py
Tests cover JSON body parsing for param/field_errors, str() formatting, constructor override precedence, and raise_for_invalid_request for 400/422 and non-matching status codes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A param went wrong, the field_errors bloomed,
The rabbit parsed JSON and errors were groomed.
from_response now catches the 400 despair,
raise_for_invalid_request handles with care.
No field goes unnamed — the rabbit declared! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding InvalidRequestError field-level validation details.
Description check ✅ Passed The description covers the change, issue, tests, and verification, with only optional template sections left incomplete.
Linked Issues check ✅ Passed The PR satisfies #17 by adding param/field_errors parsing, raising on 400/422, and including param in str().
Out of Scope Changes check ✅ Passed The changes stay focused on error handling and matching tests, with no unrelated additions visible.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@codebestia codebestia merged commit 5634c68 into ShadeProtocol:main Jun 28, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/shade/errors.py (1)

176-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the shadow _parse_body redefinition.

This copy silently replaces the earlier _parse_body at Lines 153-160. Keeping both definitions makes future edits easy to miss and looks like leftover conflict resolution.

🤖 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/shade/errors.py` around lines 176 - 183, Remove the duplicate _parse_body
definition in the same module so only the original helper remains; the redefined
copy near the later _parse_body function is shadowing the earlier implementation
and should be deleted. Keep the canonical _parse_body used by the surrounding
error-handling code in src/shade/errors.py, and make sure any callers continue
to rely on that single definition.
🤖 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.

Inline comments:
In `@src/shade/errors.py`:
- Around line 186-205: The `_parse_error_response` helper is discarding
string-valued `error` payloads, so invalid request responses like
`{"error":"missing amount"}` lose the server message. Update
`_parse_error_response` to preserve `error` when it is a string (while still
handling dict payloads for `message`, `param`, and `field_errors`), and make
sure the returned `message` falls back to that string before using the generic
invalid-request text.

---

Nitpick comments:
In `@src/shade/errors.py`:
- Around line 176-183: Remove the duplicate _parse_body definition in the same
module so only the original helper remains; the redefined copy near the later
_parse_body function is shadowing the earlier implementation and should be
deleted. Keep the canonical _parse_body used by the surrounding error-handling
code in src/shade/errors.py, and make sure any callers continue to rely on that
single definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 897abbf3-b31f-44da-adc0-2ee2c880a2ca

📥 Commits

Reviewing files that changed from the base of the PR and between 9271dd3 and ed59f0b.

📒 Files selected for processing (2)
  • src/shade/errors.py
  • tests/test_errors.py

Comment thread src/shade/errors.py
Comment on lines +186 to +205
def _parse_error_response(response_body: Optional[str]) -> dict[str, Any]:
data = _parse_body(response_body)
error = data.get("error", {})
if not isinstance(error, dict):
error = {}

field_errors = error.get("field_errors")
if not isinstance(field_errors, dict):
field_errors = data.get("field_errors")
if not isinstance(field_errors, dict):
field_errors = {}

message = error.get("message")
if not message:
message = data.get("message")

return {
"message": message,
"param": error.get("param"),
"field_errors": field_errors,

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve string-valued error messages when parsing invalid requests.

Line 188 drops any non-dict error payload, so a 400/422 body like {"error":"missing amount"} now degrades to the generic "Invalid request" instead of surfacing the server message.

Suggested fix
 def _parse_error_response(response_body: Optional[str]) -> dict[str, Any]:
     data = _parse_body(response_body)
-    error = data.get("error", {})
-    if not isinstance(error, dict):
-        error = {}
+    raw_error = data.get("error", {})
+    if isinstance(raw_error, dict):
+        error = raw_error
+    else:
+        error = {}

     field_errors = error.get("field_errors")
     if not isinstance(field_errors, dict):
         field_errors = data.get("field_errors")
     if not isinstance(field_errors, dict):
         field_errors = {}

     message = error.get("message")
+    if not message and isinstance(raw_error, str):
+        message = raw_error
     if not message:
         message = data.get("message")

     return {
         "message": message,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _parse_error_response(response_body: Optional[str]) -> dict[str, Any]:
data = _parse_body(response_body)
error = data.get("error", {})
if not isinstance(error, dict):
error = {}
field_errors = error.get("field_errors")
if not isinstance(field_errors, dict):
field_errors = data.get("field_errors")
if not isinstance(field_errors, dict):
field_errors = {}
message = error.get("message")
if not message:
message = data.get("message")
return {
"message": message,
"param": error.get("param"),
"field_errors": field_errors,
def _parse_error_response(response_body: Optional[str]) -> dict[str, Any]:
data = _parse_body(response_body)
raw_error = data.get("error", {})
if isinstance(raw_error, dict):
error = raw_error
else:
error = {}
field_errors = error.get("field_errors")
if not isinstance(field_errors, dict):
field_errors = data.get("field_errors")
if not isinstance(field_errors, dict):
field_errors = {}
message = error.get("message")
if not message and isinstance(raw_error, str):
message = raw_error
if not message:
message = data.get("message")
return {
"message": message,
"param": error.get("param"),
"field_errors": field_errors,
🤖 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/shade/errors.py` around lines 186 - 205, The `_parse_error_response`
helper is discarding string-valued `error` payloads, so invalid request
responses like `{"error":"missing amount"}` lose the server message. Update
`_parse_error_response` to preserve `error` when it is a string (while still
handling dict payloads for `message`, `param`, and `field_errors`), and make
sure the returned `message` falls back to that string before using the generic
invalid-request text.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement InvalidRequestError

2 participants