-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add GatewayError for Blaxel gateway-synthesized errors #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Stable error codes emitted by the Blaxel gateway proxy. | ||
|
|
||
| These codes appear in the ``X-Blaxel-Error-Code`` response header and the | ||
| ``error.code`` field of the JSON body on gateway-synthesized error responses. | ||
|
|
||
| Usage:: | ||
|
|
||
| from blaxel.core.client.errors import GatewayError | ||
| from blaxel.core.client.error_codes import WORKLOAD_UNAVAILABLE | ||
|
|
||
| try: | ||
| result = await some_api_call() | ||
| except GatewayError as exc: | ||
| if exc.error_code == WORKLOAD_UNAVAILABLE: | ||
| # retry with backoff | ||
| ... | ||
| """ | ||
|
|
||
| ROUTE_NOT_FOUND: str = "ROUTE_NOT_FOUND" | ||
| WORKLOAD_NOT_FOUND: str = "WORKLOAD_NOT_FOUND" | ||
| WORKSPACE_NOT_FOUND: str = "WORKSPACE_NOT_FOUND" | ||
| WORKLOAD_UNAVAILABLE: str = "WORKLOAD_UNAVAILABLE" | ||
| AUTHENTICATION_REQUIRED: str = "AUTHENTICATION_REQUIRED" | ||
| AUTHENTICATION_FAILED: str = "AUTHENTICATION_FAILED" | ||
| FORBIDDEN: str = "FORBIDDEN" | ||
| BAD_REQUEST: str = "BAD_REQUEST" | ||
| USAGE_LIMIT_EXCEEDED: str = "USAGE_LIMIT_EXCEEDED" | ||
| POLICY_VIOLATION: str = "POLICY_VIOLATION" | ||
|
|
||
| __all__ = [ | ||
| "ROUTE_NOT_FOUND", | ||
| "WORKLOAD_NOT_FOUND", | ||
| "WORKSPACE_NOT_FOUND", | ||
| "WORKLOAD_UNAVAILABLE", | ||
| "AUTHENTICATION_REQUIRED", | ||
| "AUTHENTICATION_FAILED", | ||
| "FORBIDDEN", | ||
| "BAD_REQUEST", | ||
| "USAGE_LIMIT_EXCEEDED", | ||
| "POLICY_VIOLATION", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,12 @@ | ||||||||||||||||||||||||||||||||||||||||
| """Contains shared errors types that can be raised from API functions""" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import httpx | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class UnexpectedStatus(Exception): | ||||||||||||||||||||||||||||||||||||||||
| """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -13,4 +20,67 @@ def __init__(self, status_code: int, content: bytes): | |||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| __all__ = ["UnexpectedStatus"] | ||||||||||||||||||||||||||||||||||||||||
| class GatewayError(Exception): | ||||||||||||||||||||||||||||||||||||||||
| """Raised when the Blaxel gateway proxy synthesizes an error response. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| The gateway sets ``X-Blaxel-Source: platform`` on every response it | ||||||||||||||||||||||||||||||||||||||||
| generates itself (as opposed to forwarding from the upstream workload). | ||||||||||||||||||||||||||||||||||||||||
| This exception exposes the stable error code and agent-readable metadata | ||||||||||||||||||||||||||||||||||||||||
| so callers can branch on ``error_code`` instead of parsing free-text | ||||||||||||||||||||||||||||||||||||||||
| messages. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||||||
| error_code: str, | ||||||||||||||||||||||||||||||||||||||||
| message: str, | ||||||||||||||||||||||||||||||||||||||||
| status_code: int, | ||||||||||||||||||||||||||||||||||||||||
| retryable: bool, | ||||||||||||||||||||||||||||||||||||||||
| action: str, | ||||||||||||||||||||||||||||||||||||||||
| do_not: str | None = None, | ||||||||||||||||||||||||||||||||||||||||
| docs_url: str | None = None, | ||||||||||||||||||||||||||||||||||||||||
| response: httpx.Response, | ||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||
| super().__init__(message) | ||||||||||||||||||||||||||||||||||||||||
| self.error_code = error_code | ||||||||||||||||||||||||||||||||||||||||
| self.status_code = status_code | ||||||||||||||||||||||||||||||||||||||||
| self.retryable = retryable | ||||||||||||||||||||||||||||||||||||||||
| self.action = action | ||||||||||||||||||||||||||||||||||||||||
| self.do_not = do_not | ||||||||||||||||||||||||||||||||||||||||
| self.docs_url = docs_url | ||||||||||||||||||||||||||||||||||||||||
| self.response = response | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def check_gateway_error(response: httpx.Response) -> None: | ||||||||||||||||||||||||||||||||||||||||
| """Raise :class:`GatewayError` if *response* is a gateway-synthesized error. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Call this before any other response parsing so that gateway errors are | ||||||||||||||||||||||||||||||||||||||||
| surfaced consistently across generated and hand-written API calls. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| if response.headers.get("X-Blaxel-Source") != "platform": | ||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bug (P0): Suggested change
Suggested change
Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3d73de8 — added |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| error_obj: dict[str, Any] = {} | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| body = response.json() | ||||||||||||||||||||||||||||||||||||||||
| if isinstance(body, dict): | ||||||||||||||||||||||||||||||||||||||||
| error_obj = body.get("error", {}) | ||||||||||||||||||||||||||||||||||||||||
| if not isinstance(error_obj, dict): | ||||||||||||||||||||||||||||||||||||||||
| error_obj = {} | ||||||||||||||||||||||||||||||||||||||||
| except (json.JSONDecodeError, ValueError): | ||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bug (P0): httpx fires response event hooks before the body is read ( Suggested change
Suggested change
Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3d73de8 — added |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| raise GatewayError( | ||||||||||||||||||||||||||||||||||||||||
| error_code=response.headers.get("X-Blaxel-Error-Code", ""), | ||||||||||||||||||||||||||||||||||||||||
| message=error_obj.get("message", response.text), | ||||||||||||||||||||||||||||||||||||||||
| status_code=response.status_code, | ||||||||||||||||||||||||||||||||||||||||
| retryable=bool(error_obj.get("retryable", False)), | ||||||||||||||||||||||||||||||||||||||||
| action=error_obj.get("action", ""), | ||||||||||||||||||||||||||||||||||||||||
| do_not=error_obj.get("do_not"), | ||||||||||||||||||||||||||||||||||||||||
| docs_url=error_obj.get("docs_url"), | ||||||||||||||||||||||||||||||||||||||||
| response=response, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| __all__ = ["UnexpectedStatus", "GatewayError", "check_gateway_error"] | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bug (P0): The async response hook must
await response.aread()before accessing the body. The sharedcheck_gateway_error()callsresponse.read()(sync), which does not read the body in an async context and will raiseResponseNotRead.Suggested change
Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 3d73de8 — async hook now calls
await response.aread()beforecheck_gateway_error().