Repository: Azure/azure-sdk-for-python (Python)

Issue:
[corehttp] `HttpResponseError` lacks kwargs signature `model` compared with same class of `azure-core`
[HttpResponse](https://github.com/Azure/azure-sdk-for-python/blob/ce16093018384b3601cde650bed77c4f920b55a1/sdk/core/corehttp/corehttp/exceptions.py#L177C7-L207) lacks kwargs signature `model` compared with same class of [azure-core](https://github.com/Azure/azure-sdk-for-python/blob/ce16093018384b3601cde650bed77c4f920b55a1/sdk/core/azure-core/azure/core/exceptions.py#L377-L382)

- corehttp:
![Image](https://github.com/user-attachments/assets/15941bf0-e7f8-4b65-a575-44b640c34fe9)

- azure-core:
![Image](https://github.com/user-attachments/assets/883f8afe-88f8-4c1d-aaee-b9459beb55d4)



Discussion:
@kashifkhan @xiangyan99
Thank you for your feedback. Tagging and routing to the team member best able to assist.
Not sure whether it is by design, but I think model signature is important. For example, when Typespec defines customized error like:
```
@error
model PetStoreError {
  code: int32;
  message: string;
}
```
([typepsec link](https://github.com/allenjzhang/typespec-e2e-demo/blob/81049c17926f51af863f6e6d79363ca2a9891081/petstore/spec/main.tsp#L13-L17))
The generated SDK will try to deserialize the response body with PetStoreError when error occurs then set it in model signature
```python
            map_error(status_code=response.status_code, response=response, error_map=error_map)
            error = _failsafe_deserialize(_models.PetStoreError, response.json())
            raise HttpResponseError(response=response, model=error)
```
([generated SDK link](https://github.com/allenjzhang/typespec-e2e-demo/blob/81049c17926f51af863f6e6d79363ca2a9891081/petstore/clients/python/petstore/operations/_operations.py#L573-L575))

so that SDK users could access the user-defined error model easily like:
```python
try:
   client.op(...)
except HttpResponseError as err:
    print(err.model.code)
    print(err.model.message)
```

However, if `corehttp` doesn't have model signature for `HttpResponseError`, there is no other way to set the deserialized model for SDK users.
Hey @msyyc, sorry for the delay in getting to this. I don't recall the specific reasoning for why `model` was removed from `HttpResponseError` in `corehttp`. Potentially, it was just to keep things slim initially. However, we can definitely look into adding it if it would provide some use. Some questions I have are:

1. What would the `model` be typed as? `Any`? In azure-core, we have `msrest.serialization.Model` listed as the type, however that's certainly not applicable here.
2. Is reading the `model` from the `HttpResponseError` commonly done by users? Seems the end user would need to know the model and model fields ahead of time, as I don't think Intellisense would help with that since the typing is likely arbitrary.
> Hey [@msyyc](https://github.com/msyyc), sorry for the delay in getting to this. I don't recall the specific reasoning for why `model` was removed from `HttpResponseError` in `corehttp`. Potentially, it was just to keep things slim initially. However, we can definitely look into adding it if it would provide some use. Some questions I have are:
> 
> 1. What would the `model` be typed as? `Any`? In azure-core, we have `msrest.serialization.Model` listed as the type, however that's certainly not applicable here.
> 2. Is reading the `model` from the `HttpResponseError` commonly done by users? Seems the end user would need to know the model and model fields ahead of time, as I don't think Intellisense would help with that since the typing is likely arbitrary.

1. I think it should be `MutableMapping[str, typing.Any]` which is parent class of `Model` in `_model_base.py`: https://github.com/Azure/autorest.python/blob/9519669e60300c06a8052db1b90e3a4ba1c6cdd0/packages/typespec-python/test/azure/generated/routes/routes/_model_base.py#L540
2. Without the `model`, users have to access specific attribute like `response_error.response.json()["xxx"]`. Compared with `response_error.xxx`, it is a little not convenient. As for IntelliSense, both ways can't provide IntelliSense. Above all, `model` could provide better usage experience I think. @iscai-msft / @tadelesh for awareness.
one real usage for unbranded sample code: https://github.com/allenjzhang/typespec-e2e-demo/blob/a9759159241d876891e31c20bf24009160121965/todoApp/clients/python/test/test_todo_app.py#L62
Created a PR: https://github.com/Azure/azure-sdk-for-python/issues/38956, but still a bit unsure of the typing for `model`. Typing it as a `Mapping`/`MutableMapping` would cause type-checkers/linters to complain if a user tries to access info using attribute access (i.e. `err.model.some_attribute` instead of `err.model["some_attribute"]`). Opting to keep it as `Any` for now unless we can find a better option.



Patch:
diff --git a/sdk/core/corehttp/CHANGELOG.md b/sdk/core/corehttp/CHANGELOG.md
index 8a4d87644799..8f3d5e2965bb 100644
--- a/sdk/core/corehttp/CHANGELOG.md
+++ b/sdk/core/corehttp/CHANGELOG.md
@@ -9,6 +9,7 @@
 - Added a new `AccessTokenInfo` class, which is returned by `get_token_info` implementations. This class contains the token, its expiration time, and optional additional information like when a token should be refreshed. [#38346](https://github.com/Azure/azure-sdk-for-python/pull/38346)
 - `BearerTokenCredentialPolicy` and `AsyncBearerTokenCredentialPolicy` now check if a credential has the `get_token_info` method defined. If so, the `get_token_info` method is used to acquire a token. [#38346](https://github.com/Azure/azure-sdk-for-python/pull/38346)
   - These policies now also check the `refresh_on` attribute when determining if a new token request should be made.
+- Added `model` attribute to `HttpResponseError` to allow accessing error attributes based on a known model. [#39636](https://github.com/Azure/azure-sdk-for-python/pull/39636)
 
 ### Breaking Changes
 
diff --git a/sdk/core/corehttp/corehttp/exceptions.py b/sdk/core/corehttp/corehttp/exceptions.py
index 1b2a9e324dbb..a44e8edef12e 100644
--- a/sdk/core/corehttp/corehttp/exceptions.py
+++ b/sdk/core/corehttp/corehttp/exceptions.py
@@ -187,6 +187,8 @@ class HttpResponseError(BaseError):
     :vartype status_code: int
     :ivar response: The response that triggered the exception.
     :vartype response: ~corehttp.rest.HttpResponse or ~corehttp.rest.AsyncHttpResponse
+    :ivar model: The response body model
+    :vartype model: Any
     """
 
     def __init__(
@@ -200,6 +202,8 @@ def __init__(
             self.reason = response.reason
             self.status_code = response.status_code
 
+        self.model: Optional[Any] = kwargs.pop("model", None)
+
         # By priority, message is:
         # - parameter "message", OR
         # - generic message using "reason"


Test patch:
diff --git a/sdk/core/corehttp/tests/test_exceptions.py b/sdk/core/corehttp/tests/test_exceptions.py
index 575c92a5754c..a5b85cc9022f 100644
--- a/sdk/core/corehttp/tests/test_exceptions.py
+++ b/sdk/core/corehttp/tests/test_exceptions.py
@@ -72,6 +72,20 @@ def test_httpresponse_error_with_response(self, port, transport):
         assert error.reason == "OK"
         assert isinstance(error.status_code, int)
 
+    @pytest.mark.parametrize("transport", SYNC_TRANSPORTS)
+    def test_httpresponse_error_with_model(self, port, transport):
+        request = HttpRequest("GET", url="http://localhost:{}/basic/string".format(port))
+        client = MockRestClient(port, transport=transport())
+        response = client.send_request(request, stream=False)
+        error = HttpResponseError(response=response, model=FakeErrorTwo())
+        assert error.message == "Operation returned an invalid status 'OK'"
+        assert error.response is not None
+        assert error.reason == "OK"
+        assert error.model
+        assert error.model.code == "FakeErrorTwo"
+        assert error.model.message == "A different fake error"
+        assert isinstance(error.status_code, int)
+
     @pytest.mark.parametrize("transport", SYNC_TRANSPORTS)
     def test_malformed_json(self, port, transport):
         request = HttpRequest("GET", "/errors/malformed-json")