Skip to content

Commit 0ada1cc

Browse files
elizjomayoormrDzurb
authoredApr 3, 2025··
Improved AQUA Error Messages for Authorization and Tag-Related Uses
Co-authored-by: Mayoor Rao <mayoor.rao@oracle.com> Co-authored-by: Dmitrii Cherkasov <dmitrii.cherkasov@oracle.com>
1 parent 9704fef commit 0ada1cc

File tree

8 files changed

+392
-140
lines changed

8 files changed

+392
-140
lines changed
 

‎ads/aqua/constants.py

+25
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
HF_METADATA_FOLDER = ".cache/"
4141
HF_LOGIN_DEFAULT_TIMEOUT = 2
4242
MODEL_NAME_DELIMITER = ";"
43+
AQUA_TROUBLESHOOTING_LINK = "https://github.com/oracle-samples/oci-data-science-ai-samples/blob/main/ai-quick-actions/troubleshooting-tips.md"
4344

4445
TRAINING_METRICS_FINAL = "training_metrics_final"
4546
VALIDATION_METRICS_FINAL = "validation_metrics_final"
@@ -85,3 +86,27 @@
8586
"--host",
8687
}
8788
TEI_CONTAINER_DEFAULT_HOST = "8080"
89+
90+
OCI_OPERATION_FAILURES = {
91+
"list_model_deployments": "Unable to list model deployments. See tips for troubleshooting: ",
92+
"list_models": "Unable to list models. See tips for troubleshooting: ",
93+
"get_namespace": "Unable to access specified Object Storage Bucket. See tips for troubleshooting: ",
94+
"list_log_groups":"Unable to access logs. See tips for troubleshooting: " ,
95+
"list_buckets": "Unable to list Object Storage Bucket. See tips for troubleshooting: ",
96+
"put_object": "Unable to access or find Object Storage Bucket. See tips for troubleshooting: ",
97+
"list_model_version_sets": "Unable to create or fetch model version set. See tips for troubleshooting:",
98+
"update_model": "Unable to update model. See tips for troubleshooting: ",
99+
"list_data_science_private_endpoints": "Unable to access private endpoint. See tips for troubleshooting: ",
100+
"create_model" : "Unable to register model. See tips for troubleshooting: ",
101+
"create_deployment": "Unable to create deployment. See tips for troubleshooting: ",
102+
"create_model_version_sets" : "Unable to create model version set. See tips for troubleshooting: ",
103+
"create_job": "Unable to create job. See tips for troubleshooting: ",
104+
"create_job_run": "Unable to create job run. See tips for troubleshooting: ",
105+
}
106+
107+
STATUS_CODE_MESSAGES = {
108+
"400": "Could not process your request due to invalid input.",
109+
"403": "We're having trouble processing your request with the information provided.",
110+
"404": "Authorization Failed: The resource you're looking for isn't accessible.",
111+
"408": "Server is taking too long to respond, please try again.",
112+
}

‎ads/aqua/extension/aqua_ws_msg_handler.py

+6-36
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,18 @@
33
# Copyright (c) 2024, 2025 Oracle and/or its affiliates.
44
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
55

6-
import traceback
7-
import uuid
86
from abc import abstractmethod
9-
from http.client import responses
107
from typing import List
118

12-
from tornado.web import HTTPError
13-
14-
from ads.aqua import logger
159
from ads.aqua.common.decorator import handle_exceptions
16-
from ads.aqua.extension.base_handler import AquaAPIhandler
1710
from ads.aqua.extension.models.ws_models import (
1811
AquaWsError,
1912
BaseRequest,
2013
BaseResponse,
2114
ErrorResponse,
2215
RequestResponseType,
2316
)
17+
from ads.aqua.extension.utils import construct_error
2418
from ads.config import AQUA_TELEMETRY_BUCKET, AQUA_TELEMETRY_BUCKET_NS
2519
from ads.telemetry.client import TelemetryClient
2620

@@ -55,48 +49,24 @@ def process(self) -> BaseResponse:
5549

5650
def write_error(self, status_code, **kwargs):
5751
"""AquaWSMSGhandler errors are JSON, not human pages."""
58-
reason = kwargs.get("reason")
52+
5953
service_payload = kwargs.get("service_payload", {})
60-
default_msg = responses.get(status_code, "Unknown HTTP Error")
61-
message = AquaAPIhandler.get_default_error_messages(
62-
service_payload, str(status_code), kwargs.get("message", default_msg)
63-
)
64-
reply = {
65-
"status": status_code,
66-
"message": message,
67-
"service_payload": service_payload,
68-
"reason": reason,
69-
"request_id": str(uuid.uuid4()),
70-
}
71-
exc_info = kwargs.get("exc_info")
72-
if exc_info:
73-
logger.error(
74-
f"Error Request ID: {reply['request_id']}\n"
75-
f"Error: {''.join(traceback.format_exception(*exc_info))}"
76-
)
77-
e = exc_info[1]
78-
if isinstance(e, HTTPError):
79-
reply["message"] = e.log_message or message
80-
reply["reason"] = e.reason
54+
reply_details = construct_error(status_code, **kwargs)
8155

82-
logger.error(
83-
f"Error Request ID: {reply['request_id']}\n"
84-
f"Error: {reply['message']} {reply['reason']}"
85-
)
8656
# telemetry may not be present if there is an error while initializing
8757
if hasattr(self, "telemetry"):
8858
aqua_api_details = kwargs.get("aqua_api_details", {})
8959
self.telemetry.record_event_async(
9060
category="aqua/error",
9161
action=str(status_code),
92-
value=reason,
62+
value=reply_details.reason,
9363
**aqua_api_details,
9464
)
9565
response = AquaWsError(
9666
status=status_code,
97-
message=message,
67+
message=reply_details.message,
9868
service_payload=service_payload,
99-
reason=reason,
69+
reason=reply_details.reason,
10070
)
10171
base_message = BaseRequest.from_json(self.message, ignore_unknown=True)
10272
return ErrorResponse(

‎ads/aqua/extension/base_handler.py

+8-70
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,16 @@
22
# Copyright (c) 2024, 2025 Oracle and/or its affiliates.
33
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
44

5-
65
import json
7-
import traceback
8-
import uuid
96
from dataclasses import asdict, is_dataclass
10-
from http.client import responses
117
from typing import Any
128

139
from notebook.base.handlers import APIHandler
1410
from tornado import httputil
15-
from tornado.web import Application, HTTPError
11+
from tornado.web import Application
1612

17-
from ads.aqua import logger
1813
from ads.aqua.common.utils import is_pydantic_model
14+
from ads.aqua.extension.utils import construct_error
1915
from ads.config import AQUA_TELEMETRY_BUCKET, AQUA_TELEMETRY_BUCKET_NS
2016
from ads.telemetry.client import TelemetryClient
2117

@@ -75,78 +71,20 @@ def finish(self, payload=None): # pylint: disable=W0221
7571

7672
def write_error(self, status_code, **kwargs):
7773
"""AquaAPIhandler errors are JSON, not human pages."""
78-
self.set_header("Content-Type", "application/json")
79-
reason = kwargs.get("reason")
80-
self.set_status(status_code, reason=reason)
81-
service_payload = kwargs.get("service_payload", {})
82-
default_msg = responses.get(status_code, "Unknown HTTP Error")
83-
message = self.get_default_error_messages(
84-
service_payload, str(status_code), kwargs.get("message", default_msg)
85-
)
86-
87-
reply = {
88-
"status": status_code,
89-
"message": message,
90-
"service_payload": service_payload,
91-
"reason": reason,
92-
"request_id": str(uuid.uuid4()),
93-
}
94-
exc_info = kwargs.get("exc_info")
95-
if exc_info:
96-
logger.error(
97-
f"Error Request ID: {reply['request_id']}\n"
98-
f"Error: {''.join(traceback.format_exception(*exc_info))}"
99-
)
100-
e = exc_info[1]
101-
if isinstance(e, HTTPError):
102-
reply["message"] = e.log_message or message
103-
reply["reason"] = e.reason if e.reason else reply["reason"]
10474

105-
logger.error(
106-
f"Error Request ID: {reply['request_id']}\n"
107-
f"Error: {reply['message']} {reply['reason']}"
108-
)
75+
reply_details = construct_error(status_code, **kwargs)
76+
77+
self.set_header("Content-Type", "application/json")
78+
self.set_status(status_code, reason=reply_details.reason)
10979

11080
# telemetry may not be present if there is an error while initializing
11181
if hasattr(self, "telemetry"):
11282
aqua_api_details = kwargs.get("aqua_api_details", {})
11383
self.telemetry.record_event_async(
11484
category="aqua/error",
11585
action=str(status_code),
116-
value=reason,
86+
value=reply_details.reason,
11787
**aqua_api_details,
11888
)
11989

120-
self.finish(json.dumps(reply))
121-
122-
@staticmethod
123-
def get_default_error_messages(
124-
service_payload: dict,
125-
status_code: str,
126-
default_msg: str = "Unknown HTTP Error.",
127-
):
128-
"""Method that maps the error messages based on the operation performed or the status codes encountered."""
129-
130-
messages = {
131-
"400": "Something went wrong with your request.",
132-
"403": "We're having trouble processing your request with the information provided.",
133-
"404": "Authorization Failed: The resource you're looking for isn't accessible.",
134-
"408": "Server is taking too long to response, please try again.",
135-
"create": "Authorization Failed: Could not create resource.",
136-
"get": "Authorization Failed: The resource you're looking for isn't accessible.",
137-
}
138-
139-
if service_payload and "operation_name" in service_payload:
140-
operation_name = service_payload["operation_name"]
141-
if operation_name:
142-
if operation_name.startswith("create"):
143-
return messages["create"] + f" Operation Name: {operation_name}."
144-
elif operation_name.startswith("list") or operation_name.startswith(
145-
"get"
146-
):
147-
return messages["get"] + f" Operation Name: {operation_name}."
148-
149-
if status_code in messages:
150-
return messages[status_code]
151-
else:
152-
return default_msg
90+
self.finish(reply_details)

‎ads/aqua/extension/errors.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
#!/usr/bin/env python
22
# Copyright (c) 2024 Oracle and/or its affiliates.
33
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
4+
import uuid
5+
from typing import Any, Dict, List, Optional
46

7+
from pydantic import Field
8+
9+
from ads.aqua.config.utils.serializer import Serializable
10+
11+
from ads.aqua.constants import (
12+
AQUA_TROUBLESHOOTING_LINK
13+
)
514

615
class Errors(str):
716
INVALID_INPUT_DATA_FORMAT = "Invalid format of input data."
817
NO_INPUT_DATA = "No input data provided."
918
MISSING_REQUIRED_PARAMETER = "Missing required parameter: '{}'"
1019
MISSING_ONEOF_REQUIRED_PARAMETER = "Either '{}' or '{}' is required."
1120
INVALID_VALUE_OF_PARAMETER = "Invalid value of parameter: '{}'"
21+
22+
class ReplyDetails(Serializable):
23+
"""Structured reply to be returned to the client."""
24+
status: int
25+
troubleshooting_tips: str = Field(f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
26+
description="GitHub Link for troubleshooting documentation")
27+
message: str = Field("Unknown HTTP Error.", description="GitHub Link for troubleshooting documentation")
28+
service_payload: Optional[Dict[str, Any]] = Field(default_factory=dict)
29+
reason: str = Field("Unknown error", description="Reason for Error")
30+
request_id: str = Field(str(uuid.uuid4()), description="Unique ID for tracking the error.")

‎ads/aqua/extension/utils.py

+114-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
#!/usr/bin/env python
22
# Copyright (c) 2024 Oracle and/or its affiliates.
33
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
4+
5+
import re
6+
import traceback
7+
import uuid
48
from dataclasses import fields
59
from datetime import datetime, timedelta
10+
from http.client import responses
611
from typing import Dict, Optional
712

813
from cachetools import TTLCache, cached
914
from tornado.web import HTTPError
1015

11-
from ads.aqua import ODSC_MODEL_COMPARTMENT_OCID
16+
from ads.aqua import ODSC_MODEL_COMPARTMENT_OCID, logger
1217
from ads.aqua.common.utils import fetch_service_compartment
13-
from ads.aqua.extension.errors import Errors
18+
from ads.aqua.constants import (
19+
AQUA_TROUBLESHOOTING_LINK,
20+
OCI_OPERATION_FAILURES,
21+
STATUS_CODE_MESSAGES,
22+
)
23+
from ads.aqua.extension.errors import Errors, ReplyDetails
1424

1525

1626
def validate_function_parameters(data_class, input_data: Dict):
@@ -32,3 +42,105 @@ def ui_compatability_check():
3242
fetched from the configuration. The cached result is returned when multiple calls are made in quick succession
3343
from the UI to avoid multiple config file loads."""
3444
return ODSC_MODEL_COMPARTMENT_OCID or fetch_service_compartment()
45+
46+
47+
def get_default_error_messages(
48+
service_payload: dict,
49+
status_code: str,
50+
default_msg: str = "Unknown HTTP Error.",
51+
)-> str:
52+
"""Method that maps the error messages based on the operation performed or the status codes encountered."""
53+
54+
if service_payload and "operation_name" in service_payload:
55+
operation_name = service_payload.get("operation_name")
56+
57+
if operation_name and status_code in STATUS_CODE_MESSAGES:
58+
return f"{STATUS_CODE_MESSAGES[status_code]}\n{service_payload.get('message')}\nOperation Name: {operation_name}."
59+
60+
return STATUS_CODE_MESSAGES.get(status_code, default_msg)
61+
62+
63+
def get_documentation_link(key: str) -> str:
64+
"""Generates appropriate GitHub link to AQUA Troubleshooting Documentation per the user's error."""
65+
github_header = re.sub(r"_", "-", key)
66+
return f"{AQUA_TROUBLESHOOTING_LINK}#{github_header}"
67+
68+
69+
def get_troubleshooting_tips(service_payload: dict,
70+
status_code: str) -> str:
71+
"""Maps authorization errors to potential solutions on Troubleshooting Page per Aqua Documentation on oci-data-science-ai-samples"""
72+
73+
tip = f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}"
74+
75+
if status_code in (404, 400):
76+
failed_operation = service_payload.get('operation_name')
77+
78+
if failed_operation in OCI_OPERATION_FAILURES:
79+
link = get_documentation_link(failed_operation)
80+
tip = OCI_OPERATION_FAILURES[failed_operation] + link
81+
82+
return tip
83+
84+
85+
def construct_error(status_code: int, **kwargs) -> ReplyDetails:
86+
"""
87+
Formats an error response based on the provided status code and optional details.
88+
89+
Args:
90+
status_code (int): The HTTP status code of the error.
91+
**kwargs: Additional optional parameters:
92+
- reason (str, optional): A brief reason for the error.
93+
- service_payload (dict, optional): Contextual error data from OCI SDK methods
94+
- message (str, optional): A custom error message, from error raised from failed AQUA methods calling OCI SDK methods
95+
- exc_info (tuple, optional): Exception information (e.g., from `sys.exc_info()`), used for logging.
96+
97+
Returns:
98+
ReplyDetails: A Pydantic object containing details about the formatted error response.
99+
kwargs:
100+
- "status" (int): The HTTP status code.
101+
- "troubleshooting_tips" (str): a GitHub link to AQUA troubleshooting docs, may be linked to a specific header.
102+
- "message" (str): error message.
103+
- "service_payload" (Dict[str, Any], optional) : Additional context from OCI Python SDK call.
104+
- "reason" (str): The reason for the error.
105+
- "request_id" (str): A unique identifier for tracking the error.
106+
107+
Logs:
108+
- Logs the error details with a unique request ID.
109+
- If `exc_info` is provided and contains an `HTTPError`, updates the response message and reason accordingly.
110+
111+
"""
112+
reason = kwargs.get("reason", "Unknown Error")
113+
service_payload = kwargs.get("service_payload", {})
114+
default_msg = responses.get(status_code, "Unknown HTTP Error")
115+
message = get_default_error_messages(
116+
service_payload, str(status_code), kwargs.get("message", default_msg)
117+
)
118+
119+
tips = get_troubleshooting_tips(service_payload, status_code)
120+
121+
122+
reply = ReplyDetails(
123+
status = status_code,
124+
troubleshooting_tips = tips,
125+
message = message,
126+
service_payload = service_payload,
127+
reason = reason,
128+
request_id = str(uuid.uuid4()),
129+
)
130+
131+
exc_info = kwargs.get("exc_info")
132+
if exc_info:
133+
logger.error(
134+
f"Error Request ID: {reply.request_id}\n"
135+
f"Error: {''.join(traceback.format_exception(*exc_info))}"
136+
)
137+
e = exc_info[1]
138+
if isinstance(e, HTTPError):
139+
reply.message = e.log_message or message
140+
reply.reason = e.reason if e.reason else reply.reason
141+
142+
logger.error(
143+
f"Error Request ID: {reply.request_id}\n"
144+
f"Error: {reply.message} {reply.reason}"
145+
)
146+
return reply

‎tests/unitary/with_extras/aqua/test_decorator.py

+142-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
from tornado.web import HTTPError
2424

2525
from ads.aqua.common.errors import AquaError
26+
from ads.aqua.constants import (
27+
AQUA_TROUBLESHOOTING_LINK,
28+
OCI_OPERATION_FAILURES,
29+
STATUS_CODE_MESSAGES,
30+
)
2631
from ads.aqua.extension.base_handler import AquaAPIhandler
32+
from ads.aqua.extension.errors import ReplyDetails
2733

2834

2935
class TestDataset:
@@ -40,6 +46,7 @@ def setUp(self, ipython_init_mock) -> None:
4046
self.test_instance.finish = MagicMock()
4147
self.test_instance.set_header = MagicMock()
4248
self.test_instance.set_status = MagicMock()
49+
self.test_instance.set_service_payload = MagicMock()
4350

4451
@parameterized.expand(
4552
[
@@ -53,6 +60,7 @@ def setUp(self, ipython_init_mock) -> None:
5360
),
5461
{
5562
"status": 500,
63+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
5664
"message": "An internal error occurred.",
5765
"service_payload": {
5866
"target_service": None,
@@ -70,13 +78,134 @@ def setUp(self, ipython_init_mock) -> None:
7078
"reason": "An internal error occurred.",
7179
"request_id": TestDataset.mock_request_id,
7280
},
81+
],
82+
[
83+
"oci ServiceError",
84+
ServiceError(
85+
status=400,
86+
code="InvalidParameter",
87+
message="Invalid tags",
88+
operation_name= "create_model",
89+
headers={},
90+
),
91+
{
92+
"status": 400,
93+
"troubleshooting_tips": f"{OCI_OPERATION_FAILURES['create_model']}{AQUA_TROUBLESHOOTING_LINK}#create-model",
94+
"message": f"{STATUS_CODE_MESSAGES['400']}\nInvalid tags\nOperation Name: create_model.",
95+
"service_payload": {
96+
"target_service": None,
97+
"status": 400,
98+
"code": "InvalidParameter",
99+
"opc-request-id": None,
100+
"message": "Invalid tags",
101+
"operation_name": "create_model",
102+
"timestamp": None,
103+
"client_version": None,
104+
"request_endpoint": None,
105+
"logging_tips": "To get more info on the failing request, refer to https://docs.oracle.com/en-us/iaas/tools/python/latest/logging.html for ways to log the request/response details.",
106+
"troubleshooting_tips": "See https://docs.oracle.com/iaas/Content/API/References/apierrors.htm#apierrors_400__400_invalidparameter for more information about resolving this error. If you are unable to resolve this None issue, please contact Oracle support and provide them this full error message.",
107+
},
108+
"reason": "Invalid tags",
109+
"request_id": TestDataset.mock_request_id,
110+
},
111+
],
112+
[
113+
"oci ServiceError",
114+
ServiceError(
115+
status=400,
116+
code="InvalidParameter",
117+
message="Invalid tags",
118+
operation_name= "update_model",
119+
headers={},
120+
),
121+
{
122+
"status": 400,
123+
"troubleshooting_tips": f"{OCI_OPERATION_FAILURES['update_model']}{AQUA_TROUBLESHOOTING_LINK}#update-model",
124+
"message": f"{STATUS_CODE_MESSAGES['400']}\nInvalid tags\nOperation Name: update_model.",
125+
"service_payload": {
126+
"target_service": None,
127+
"status": 400,
128+
"code": "InvalidParameter",
129+
"opc-request-id": None,
130+
"message": "Invalid tags",
131+
"operation_name": "update_model",
132+
"timestamp": None,
133+
"client_version": None,
134+
"request_endpoint": None,
135+
"logging_tips": "To get more info on the failing request, refer to https://docs.oracle.com/en-us/iaas/tools/python/latest/logging.html for ways to log the request/response details.",
136+
"troubleshooting_tips": "See https://docs.oracle.com/iaas/Content/API/References/apierrors.htm#apierrors_400__400_invalidparameter for more information about resolving this error. If you are unable to resolve this None issue, please contact Oracle support and provide them this full error message.",
137+
},
138+
"reason": "Invalid tags",
139+
"request_id": TestDataset.mock_request_id,
140+
},
141+
],
142+
[
143+
"oci ServiceError",
144+
ServiceError(
145+
status=400,
146+
code="InvalidParameter",
147+
message="Invalid tags",
148+
operation_name= "update_model",
149+
headers={},
150+
),
151+
{
152+
"status": 400,
153+
"troubleshooting_tips": f"{OCI_OPERATION_FAILURES['update_model']}{AQUA_TROUBLESHOOTING_LINK}#update-model",
154+
"message": f"{STATUS_CODE_MESSAGES['400']}\nInvalid tags\nOperation Name: update_model.",
155+
"service_payload": {
156+
"target_service": None,
157+
"status": 400,
158+
"code": "InvalidParameter",
159+
"opc-request-id": None,
160+
"message": "Invalid tags",
161+
"operation_name": "update_model",
162+
"timestamp": None,
163+
"client_version": None,
164+
"request_endpoint": None,
165+
"logging_tips": "To get more info on the failing request, refer to https://docs.oracle.com/en-us/iaas/tools/python/latest/logging.html for ways to log the request/response details.",
166+
"troubleshooting_tips": "See https://docs.oracle.com/iaas/Content/API/References/apierrors.htm#apierrors_400__400_invalidparameter for more information about resolving this error. If you are unable to resolve this None issue, please contact Oracle support and provide them this full error message.",
167+
},
168+
"reason": "Invalid tags",
169+
"request_id": TestDataset.mock_request_id,
170+
},
171+
],
172+
[
173+
"oci ServiceError",
174+
ServiceError(
175+
status=404,
176+
code="BucketNotFound",
177+
message="Either the bucket named 'xxx' does not exist in the namespace 'xxx' or you are not authorized to access it",
178+
operation_name= "put_object",
179+
headers={},
180+
),
181+
{
182+
"status": 404,
183+
"troubleshooting_tips": f"{OCI_OPERATION_FAILURES['put_object']}{AQUA_TROUBLESHOOTING_LINK}#put-object",
184+
"message": f"{STATUS_CODE_MESSAGES['404']}\nEither the bucket named 'xxx' does not exist in the namespace 'xxx' or you are not authorized to access it\nOperation Name: put_object.",
185+
"service_payload": {
186+
"target_service": None,
187+
"status": 404,
188+
"code": "BucketNotFound",
189+
"opc-request-id": None,
190+
"message": "Either the bucket named 'xxx' does not exist in the namespace 'xxx' or you are not authorized to access it",
191+
"operation_name": "put_object",
192+
"timestamp": None,
193+
"client_version": None,
194+
"request_endpoint": None,
195+
"logging_tips": "To get more info on the failing request, refer to https://docs.oracle.com/en-us/iaas/tools/python/latest/logging.html for ways to log the request/response details.",
196+
"troubleshooting_tips": "See https://docs.oracle.com/iaas/Content/API/References/apierrors.htm#apierrors_404__404_bucketnotfound for more information about resolving this error. If you are unable to resolve this None issue, please contact Oracle support and provide them this full error message.",
197+
},
198+
"reason": "Either the bucket named 'xxx' does not exist in the namespace 'xxx' or you are not authorized to access it" ,
199+
"request_id": TestDataset.mock_request_id,
200+
},
73201
],
74202
[
75203
"oci ClientError",
76204
ConfigFileNotFound("Could not find config file at the given path."),
77205
{
78206
"status": 400,
79-
"message": "Something went wrong with your request.",
207+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
208+
"message": STATUS_CODE_MESSAGES["400"],
80209
"service_payload": {},
81210
"reason": "ConfigFileNotFound: Could not find config file at the given path.",
82211
"request_id": TestDataset.mock_request_id,
@@ -89,7 +218,8 @@ def setUp(self, ipython_init_mock) -> None:
89218
),
90219
{
91220
"status": 400,
92-
"message": "Something went wrong with your request.",
221+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
222+
"message": STATUS_CODE_MESSAGES["400"],
93223
"service_payload": {},
94224
"reason": "MissingEndpointForNonRegionalServiceClientError: An endpoint must be provided for a non-regional service client",
95225
"request_id": TestDataset.mock_request_id,
@@ -100,7 +230,8 @@ def setUp(self, ipython_init_mock) -> None:
100230
RequestException("An exception occurred when making the request"),
101231
{
102232
"status": 400,
103-
"message": "Something went wrong with your request.",
233+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
234+
"message": STATUS_CODE_MESSAGES["400"],
104235
"service_payload": {},
105236
"reason": "RequestException: An exception occurred when making the request",
106237
"request_id": TestDataset.mock_request_id,
@@ -113,7 +244,8 @@ def setUp(self, ipython_init_mock) -> None:
113244
),
114245
{
115246
"status": 408,
116-
"message": "Server is taking too long to response, please try again.",
247+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
248+
"message": "Server is taking too long to respond, please try again.",
117249
"service_payload": {},
118250
"reason": "ConnectTimeout: The request timed out while trying to connect to the remote server.",
119251
"request_id": TestDataset.mock_request_id,
@@ -124,6 +256,7 @@ def setUp(self, ipython_init_mock) -> None:
124256
MultipartUploadError(),
125257
{
126258
"status": 500,
259+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
127260
"message": "Internal Server Error",
128261
"service_payload": {},
129262
"reason": f"MultipartUploadError: MultipartUploadError exception has occurred. {UPLOAD_MANAGER_DEBUG_INFORMATION_LOG}",
@@ -135,6 +268,7 @@ def setUp(self, ipython_init_mock) -> None:
135268
CompositeOperationError(),
136269
{
137270
"status": 500,
271+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
138272
"message": "Internal Server Error",
139273
"service_payload": {},
140274
"reason": "CompositeOperationError: ",
@@ -146,6 +280,7 @@ def setUp(self, ipython_init_mock) -> None:
146280
AquaError(reason="Mocking AQUA error.", status=403, service_payload={}),
147281
{
148282
"status": 403,
283+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
149284
"message": "We're having trouble processing your request with the information provided.",
150285
"service_payload": {},
151286
"reason": "Mocking AQUA error.",
@@ -157,6 +292,7 @@ def setUp(self, ipython_init_mock) -> None:
157292
HTTPError(400, "The request `/test` is invalid."),
158293
{
159294
"status": 400,
295+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
160296
"message": "The request `/test` is invalid.",
161297
"service_payload": {},
162298
"reason": "The request `/test` is invalid.",
@@ -168,6 +304,7 @@ def setUp(self, ipython_init_mock) -> None:
168304
ValueError("Mocking ADS internal error."),
169305
{
170306
"status": 500,
307+
"troubleshooting_tips": f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
171308
"message": "Internal Server Error",
172309
"service_payload": {},
173310
"reason": "ValueError: Mocking ADS internal error.",
@@ -182,7 +319,7 @@ def test_handle_exceptions(self, name, error, expected_reply, mock_uuid):
182319
from ads.aqua.common.decorator import handle_exceptions
183320

184321
mock_uuid.return_value = TestDataset.mock_request_id
185-
expected_call = json.dumps(expected_reply)
322+
expected_call = ReplyDetails(**expected_reply)
186323

187324
@handle_exceptions
188325
def mock_function(self):

‎tests/unitary/with_extras/aqua/test_handlers.py

+26-15
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
import ads.aqua.extension.common_handler
2323
import ads.config
2424
from ads.aqua.common.errors import AquaError
25+
from ads.aqua.constants import (
26+
AQUA_TROUBLESHOOTING_LINK,
27+
STATUS_CODE_MESSAGES,
28+
)
2529
from ads.aqua.data import AquaResourceIdentifier
2630
from ads.aqua.evaluation import AquaEvaluationApp
2731
from ads.aqua.extension.base_handler import AquaAPIhandler
@@ -38,7 +42,7 @@
3842
from ads.aqua.extension.model_handler import AquaModelHandler, AquaModelLicenseHandler
3943
from ads.aqua.model import AquaModelApp
4044
from tests.unitary.with_extras.aqua.utils import HandlerTestDataset as TestDataset
41-
45+
from ads.aqua.extension.errors import ReplyDetails
4246

4347
class TestBaseHandlers(unittest.TestCase):
4448
"""Contains test cases for base handler."""
@@ -84,6 +88,7 @@ def test_finish(self, name, payload, expected_call, mock_super_finish):
8488
"HTTPError",
8589
dict(
8690
status_code=400,
91+
reason = "Unknown Error",
8792
exc_info=(None, HTTPError(400, "Bad Request"), None),
8893
),
8994
"Bad Request",
@@ -113,11 +118,11 @@ def test_finish(self, name, payload, expected_call, mock_super_finish):
113118
None,
114119
),
115120
),
116-
"Authorization Failed: Could not create resource. Operation Name: create_resources.",
121+
f"{STATUS_CODE_MESSAGES['404']}\nThe required information to complete authentication was not provided or was incorrect.\nOperation Name: create_resources.",
117122
],
118123
[
119124
"oci ServiceError",
120-
dict(
125+
dict( # noqa: C408
121126
status_code=404,
122127
reason="Testing ServiceError happen when get_job_run.",
123128
service_payload=TestDataset.mock_service_payload_get,
@@ -139,11 +144,11 @@ def test_finish(self, name, payload, expected_call, mock_super_finish):
139144
],
140145
),
141146
),
142-
"Authorization Failed: The resource you're looking for isn't accessible. Operation Name: get_job_run.",
147+
f"{STATUS_CODE_MESSAGES['404']}\nThe required information to complete authentication was not provided or was incorrect.\nOperation Name: get_job_run.",
143148
],
144149
]
145150
)
146-
@patch("ads.aqua.extension.base_handler.logger")
151+
@patch("ads.aqua.extension.utils.logger")
147152
@patch("uuid.uuid4")
148153
def test_write_error(self, name, input, expected_msg, mock_uuid, mock_logger):
149154
"""Tests AquaAPIhandler.write_error"""
@@ -160,15 +165,19 @@ def test_write_error(self, name, input, expected_msg, mock_uuid, mock_logger):
160165
self.test_instance.set_status.assert_called_once_with(
161166
input.get("status_code"), reason=input.get("reason")
162167
)
163-
expected_reply = {
164-
"status": input.get("status_code"),
165-
"message": expected_msg,
166-
"service_payload": input.get("service_payload", {}),
167-
"reason": input.get("reason"),
168-
"request_id": "1234",
169-
}
170-
self.test_instance.finish.assert_called_once_with(json.dumps(expected_reply))
168+
expected_reply = ReplyDetails(
169+
status = input.get("status_code"),
170+
troubleshooting_tips = f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
171+
message = expected_msg,
172+
service_payload = input.get("service_payload", {}),
173+
reason = input.get("reason", "Unknown Error"),
174+
request_id = "1234",
175+
)
176+
177+
178+
self.test_instance.finish.assert_called_once_with(expected_reply)
171179
aqua_api_details = input.get("aqua_api_details", {})
180+
172181
self.test_instance.telemetry.record_event_async.assert_called_with(
173182
category="aqua/error",
174183
action=str(
@@ -177,10 +186,12 @@ def test_write_error(self, name, input, expected_msg, mock_uuid, mock_logger):
177186
value=input.get("reason"),
178187
**aqua_api_details,
179188
)
189+
180190
error_message = (
181-
f"Error Request ID: {expected_reply['request_id']}\n"
182-
f"Error: {expected_reply['message']} {expected_reply['reason']}"
191+
f"Error Request ID: {expected_reply.request_id}\n"
192+
f"Error: {expected_reply.message} {expected_reply.reason}"
183193
)
194+
184195
mock_logger.error.assert_called_with(error_message)
185196

186197

‎tests/unitary/with_extras/aqua/test_model_handler.py

+52-12
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
import pytest
1010
from huggingface_hub.hf_api import HfApi, ModelInfo
1111
from huggingface_hub.utils import GatedRepoError
12-
from notebook.base.handlers import IPythonHandler, HTTPError
12+
from notebook.base.handlers import HTTPError, IPythonHandler
1313
from parameterized import parameterized
1414

1515
from ads.aqua.common.errors import AquaRuntimeError
1616
from ads.aqua.common.utils import get_hf_model_info
17+
from ads.aqua.constants import (
18+
AQUA_TROUBLESHOOTING_LINK,
19+
STATUS_CODE_MESSAGES,
20+
)
21+
from ads.aqua.extension.errors import ReplyDetails
1722
from ads.aqua.extension.model_handler import (
1823
AquaHuggingFaceHandler,
1924
AquaModelHandler,
@@ -355,25 +360,47 @@ def test_post_negative(self, mock_uuid, mock_format_hf_custom_error_message):
355360
self.mock_handler.get_json_body = MagicMock(side_effect=ValueError())
356361
self.mock_handler.post()
357362
self.mock_handler.finish.assert_called_with(
358-
'{"status": 400, "message": "Invalid format of input data.", "service_payload": {}, "reason": "Invalid format of input data.", "request_id": "###"}'
363+
ReplyDetails(
364+
status=400,
365+
troubleshooting_tips= f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
366+
message= "Invalid format of input data.",
367+
service_payload = {},
368+
reason = "Invalid format of input data.",
369+
request_id = "###"
370+
371+
)
359372
)
360373
get_hf_model_info.cache_clear()
361374

362375
# case 2
363376
self.mock_handler.get_json_body = MagicMock(return_value={})
364377
self.mock_handler.post()
365378
self.mock_handler.finish.assert_called_with(
366-
'{"status": 400, "message": "No input data provided.", "service_payload": {}, '
367-
'"reason": "No input data provided.", "request_id": "###"}'
379+
ReplyDetails(
380+
status=400,
381+
troubleshooting_tips= f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
382+
message= "No input data provided.",
383+
service_payload = {},
384+
reason = "No input data provided.",
385+
request_id = "###"
386+
387+
)
368388
)
369389
get_hf_model_info.cache_clear()
370390

371391
# case 3
372392
self.mock_handler.get_json_body = MagicMock(return_value={"some_field": None})
373393
self.mock_handler.post()
374394
self.mock_handler.finish.assert_called_with(
375-
'{"status": 400, "message": "Missing required parameter: \'model_id\'", '
376-
'"service_payload": {}, "reason": "Missing required parameter: \'model_id\'", "request_id": "###"}'
395+
ReplyDetails(
396+
status=400,
397+
troubleshooting_tips= f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
398+
message= "Missing required parameter: \'model_id\'",
399+
service_payload = {},
400+
reason = "Missing required parameter: \'model_id\'",
401+
request_id = "###"
402+
403+
)
377404
)
378405
get_hf_model_info.cache_clear()
379406

@@ -389,8 +416,15 @@ def test_post_negative(self, mock_uuid, mock_format_hf_custom_error_message):
389416
mock_model_info.side_effect = GatedRepoError(message="test message")
390417
self.mock_handler.post()
391418
self.mock_handler.finish.assert_called_with(
392-
'{"status": 400, "message": "Something went wrong with your request.", '
393-
'"service_payload": {}, "reason": "test error message", "request_id": "###"}'
419+
ReplyDetails(
420+
status=400,
421+
troubleshooting_tips= f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
422+
message= STATUS_CODE_MESSAGES["400"],
423+
service_payload = {},
424+
reason = "test error message",
425+
request_id = "###"
426+
427+
)
394428
)
395429
get_hf_model_info.cache_clear()
396430

@@ -401,11 +435,17 @@ def test_post_negative(self, mock_uuid, mock_format_hf_custom_error_message):
401435
with patch.object(HfApi, "model_info") as mock_model_info:
402436
mock_model_info.return_value = MagicMock(disabled=True, id="test_model_id")
403437
self.mock_handler.post()
438+
404439
self.mock_handler.finish.assert_called_with(
405-
'{"status": 400, "message": "Something went wrong with your request.", "service_payload": {}, '
406-
'"reason": "The chosen model \'test_model_id\' is currently disabled and cannot be '
407-
"imported into AQUA. Please verify the model's status on the Hugging Face Model "
408-
'Hub or select a different model.", "request_id": "###"}'
440+
ReplyDetails(
441+
status=400,
442+
troubleshooting_tips= f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
443+
message= STATUS_CODE_MESSAGES["400"],
444+
service_payload = {},
445+
reason = "The chosen model \'test_model_id\' is currently disabled and cannot be imported into AQUA. Please verify the model\'s status on the Hugging Face Model Hub or select a different model.",
446+
request_id = "###"
447+
448+
)
409449
)
410450
get_hf_model_info.cache_clear()
411451

0 commit comments

Comments
 (0)
Please sign in to comment.