Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/optimization-20260507-101040.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: optimization
body: Shows explicit API error message
Comment thread
v-alexmoraru marked this conversation as resolved.
time: 2026-05-07T10:10:40.73557+03:00
custom:
Author: v-alexmoraru
AuthorLink: https://github.com/v-alexmoraru
38 changes: 28 additions & 10 deletions src/fabric_cli/core/fab_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
# Default error constants - avoids circular imports
DEFAULT_ERROR_MESSAGE = "An error occurred while processing the operation"
DEFAULT_ERROR_CODE = "UnknownError"
NOT_SET = object()


class FabricCLIError(Exception):
def __init__(self, message=None, status_code=None):
# Use default values if not provided
def __init__(self, message=None, status_code=NOT_SET):
# message: values like (None, "") fall back to the default.
# status_code: default is applied only when omitted entirely;
# an explicit None is preserved (e.g. fallback paths that have no code).
message = message or DEFAULT_ERROR_MESSAGE
status_code = status_code or DEFAULT_ERROR_CODE
if status_code is NOT_SET:
status_code = DEFAULT_ERROR_CODE

super().__init__(message)
self.message = message.rstrip(".")
Expand Down Expand Up @@ -78,12 +82,23 @@ def __init__(self, response_text):
related_resource (dict): Details about the main related resource, if available.
request_id (str): The ID of the request associated with the error.
"""
response = self._parse_json_response(response_text)

message = response.get("message")
error_code = response.get("errorCode")
self.more_details: list[dict] = response.get("moreDetails", [])
self.request_id = response.get("requestId")
try:
response = json.loads(response_text)
if not isinstance(response, dict):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think response_text can be a string, not always an object. but Im not sure. worth checking it

raise ValueError("Unexpected JSON shape")
Comment thread
v-alexmoraru marked this conversation as resolved.
message = (
response.get("message")
if response.get("message") is not None
else response_text
)
error_code = response.get("errorCode")
self.more_details: list[dict] = response.get("moreDetails", [])
self.request_id = response.get("requestId")
Comment thread
v-alexmoraru marked this conversation as resolved.
except (json.JSONDecodeError, TypeError, ValueError):
message = response_text
error_code = None
self.more_details = []
self.request_id = None
Comment thread
v-alexmoraru marked this conversation as resolved.

super().__init__(message, error_code)

Expand All @@ -105,7 +120,10 @@ def formatted_message(self, verbose=False):
else f"{base_message}\n<grey>{detailed_message}</grey>"
)

return f"{final_message}\n<grey>∟ Request Id: {self.request_id}</grey>"
if self.request_id:
final_message += f"\n<grey>∟ Request Id: {self.request_id}</grey>"

return final_message


class OnelakeAPIError(FabricCLIError):
Expand Down
69 changes: 68 additions & 1 deletion tests/test_utils/test_fab_custom_exception.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from fabric_cli.core.fab_exceptions import FabricCLIError
import json

from fabric_cli.core.fab_exceptions import FabricAPIError, FabricCLIError


def test_custom_error_message():
Expand All @@ -17,3 +19,68 @@ def test_custom_error_message_without_period():
def test_custom_error_formatted_message_with_status_code():
error = FabricCLIError("An error occurred.", status_code=404)
assert error.formatted_message() == "[404] An error occurred"


def test_fabric_api_error_valid_json_with_request_id():
payload = json.dumps(
{
"errorCode": "ItemNotFound",
"message": "The requested item was not found.",
"requestId": "abc-123",
"moreDetails": [],
}
)
error = FabricAPIError(payload)

assert error.status_code == "ItemNotFound"
assert error.message == "The requested item was not found"
assert error.request_id == "abc-123"
assert error.more_details == []


def test_fabric_api_error_valid_json_without_request_id():
payload = json.dumps(
{
"errorCode": "Unauthorized",
"message": "Access denied.",
}
)
error = FabricAPIError(payload)

assert error.status_code == "Unauthorized"
assert error.request_id is None
# formatted_message should not append a request-id line
assert "Request Id" not in error.formatted_message(verbose=True)


def test_fabric_api_error_non_json_body_falls_back_to_raw_text():
raw = "Internal Server Error"
error = FabricAPIError(raw)

assert error.message == raw.rstrip(".")
assert error.status_code is None
assert error.request_id is None
assert error.more_details == []


def test_fabric_api_error_non_dict_json_falls_back_to_raw_text():
for raw in ('"just a string"', "[1, 2, 3]", "42", "true"):
error = FabricAPIError(raw)
assert error.message == raw.rstrip(".")
assert error.status_code is None
assert error.request_id is None
assert error.more_details == []


def test_fabric_api_error_formatted_message_non_json_no_request_id_line():
error = FabricAPIError("Gateway Timeout")
formatted = error.formatted_message(verbose=True)
assert "Request Id" not in formatted
assert "Gateway Timeout" in formatted


def test_fabric_api_error_none_input_falls_back_to_default_message():
error = FabricAPIError(None)
assert error.status_code is None
assert error.request_id is None
assert error.more_details == []
Loading