Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

Commit

Permalink
Merge pull request #95 from microsoft/feature/apierror-response-headers
Browse files Browse the repository at this point in the history
Include response headers in API exception
  • Loading branch information
samwelkanda authored May 5, 2023
2 parents e816a79 + 46be7df commit 01b6aa0
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @samwelkanda @baywet @darrelmiller @zengin @MichaelMainer @ddyett @shemogumbe
* @samwelkanda @baywet @darrelmiller @zengin @MichaelMainer @ddyett @shemogumbe @andrueastman @ndiritu @silaskenneth
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.2] - 2023-05-02

### Added

### Changed

- Includes Response headers in APIException for failed requests.

## [0.4.1] - 2023-03-29

### Added
Expand Down
24 changes: 20 additions & 4 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion kiota_http/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION: str = '0.4.1'
VERSION: str = '0.4.2'
31 changes: 18 additions & 13 deletions kiota_http/httpx_request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,37 +280,42 @@ async def throw_failed_responses(
if response.is_success:
return

status_code = response.status_code
status_code_str = str(response.status_code)
response_status_code = response.status_code
response_status_code_str = str(response_status_code)
response_headers = response.headers

if not error_map:
raise APIError(
"The server returned an unexpected status code and no error class is registered"
f" for this code {status_code}", status_code
f" for this code {response_status_code}", response_headers, response_status_code
)
if (status_code_str not in error_map) and (
(400 <= status_code < 500 and '4XX' not in error_map) or
(500 <= status_code < 600 and '5XX' not in error_map)
if (response_status_code_str not in error_map) and (
(400 <= response_status_code < 500 and '4XX' not in error_map) or
(500 <= response_status_code < 600 and '5XX' not in error_map)
):
raise APIError(
"The server returned an unexpected status code and no error class is registered"
f" for this code {status_code}", status_code
f" for this code {response_status_code}", response_headers, response_status_code
)

error_class = None
if status_code_str in error_map:
error_class = error_map[status_code_str]
elif 400 <= status_code < 500:
if response_status_code_str in error_map:
error_class = error_map[response_status_code_str]
elif 400 <= response_status_code < 500:
error_class = error_map['4XX']
elif 500 <= status_code < 600:
elif 500 <= response_status_code < 600:
error_class = error_map['5XX']

root_node = await self.get_root_parse_node(response)
error = root_node.get_object_value(error_class)

if error:
if isinstance(error, APIError):
error.response_headers = response_headers
error.response_status_code = response_status_code
raise error
raise APIError(f"Unexpected error type: {type(error)}", status_code)
raise APIError(
f"Unexpected error type: {type(error)}", response_headers, response_status_code
)

async def get_http_response_message(self, request_info: RequestInformation) -> httpx.Response:
self.set_base_url_for_request_information(request_info)
Expand Down
96 changes: 96 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
-i https://pypi.org/simple

astroid==2.15.4 ; python_full_version >= '3.7.2'

asyncmock==0.4.2

certifi==2022.12.7 ; python_version >= '3.6'

charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'

colorama==0.4.6 ; sys_platform == 'win32'

coverage[toml]==7.2.5 ; python_version >= '3.7'

dill==0.3.6 ; python_version < '3.11'

docutils==0.19 ; python_version >= '3.7'

exceptiongroup==1.1.1 ; python_version < '3.11'

flit==3.8.0

flit-core==3.8.0 ; python_version >= '3.6'

idna==3.4 ; python_version >= '3.5'

iniconfig==2.0.0 ; python_version >= '3.7'

isort==5.12.0

lazy-object-proxy==1.9.0 ; python_version >= '3.7'

mccabe==0.7.0 ; python_version >= '3.6'

mock==5.0.2 ; python_version >= '3.6'

mypy==1.2.0

mypy-extensions==1.0.0 ; python_version >= '3.5'

packaging==23.1 ; python_version >= '3.7'

platformdirs==3.5.0 ; python_version >= '3.7'

pluggy==1.0.0 ; python_version >= '3.6'

pylint==2.17.3

pytest==7.3.1

pytest-asyncio==0.21.0

pytest-cov==4.0.0

pytest-mock==3.10.0

requests==2.29.0 ; python_version >= '3.7'

toml==0.10.2

tomli==2.0.1 ; python_version < '3.11'

tomli-w==1.0.0 ; python_version >= '3.7'

tomlkit==0.11.8 ; python_version >= '3.7'

types-python-dateutil==2.8.19.12

typing-extensions==4.5.0 ; python_version >= '3.7'

urllib3==1.26.15 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'

wrapt==1.15.0 ; python_version < '3.11'

yapf==0.33.0

anyio==3.6.2 ; python_full_version >= '3.6.2'

h11==0.14.0 ; python_version >= '3.7'

h2==4.1.0

hpack==4.0.0 ; python_full_version >= '3.6.1'

httpcore==0.17.0 ; python_version >= '3.7'

httpx[http2]==0.24.0

hyperframe==6.0.1 ; python_full_version >= '3.6.1'

microsoft-kiota-abstractions==0.5.1

sniffio==1.3.0 ; python_version >= '3.7'

uritemplate==4.1.1 ; python_version >= '3.6'

24 changes: 23 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import httpx
import pytest
from asyncmock import AsyncMock
from kiota_abstractions.api_error import APIError
from kiota_abstractions.authentication import AnonymousAuthenticationProvider
from kiota_abstractions.request_information import RequestInformation

Expand Down Expand Up @@ -44,6 +45,20 @@ def mock_error_map():
}


@pytest.fixture
def mock_apierror_map():
return {
"500":
APIError(
"Custom Internal Server Error", {
'cache-control': 'private',
'transfer-encoding': 'chunked',
'content-type': 'application/json'
}, 500
)
}


@pytest.fixture
def mock_request_adapter():
resp = httpx.Response(
Expand All @@ -54,12 +69,19 @@ def mock_request_adapter():


@pytest.fixture
def simple_response():
def simple_error_response():
return httpx.Response(
json={'error': 'not found'}, status_code=404, headers={"Content-Type": "application/json"}
)


@pytest.fixture
def simple_success_response():
return httpx.Response(
json={'message': 'Success!'}, status_code=200, headers={"Content-Type": "application/json"}
)


@pytest.fixture
def mock_user_response(mocker):
return httpx.Response(
Expand Down
62 changes: 43 additions & 19 deletions tests/test_httpx_request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ def test_get_serialization_writer_factory(request_adapter):
)


def test_get_response_content_type(request_adapter, simple_response):
content_type = request_adapter.get_response_content_type(simple_response)
def test_get_response_content_type(request_adapter, simple_success_response):
content_type = request_adapter.get_response_content_type(simple_success_response)
assert content_type == 'application/json'


Expand Down Expand Up @@ -83,48 +83,59 @@ def test_enable_backing_store(request_adapter):


@pytest.mark.asyncio
async def test_get_root_parse_node(request_adapter, simple_response):
assert simple_response.text == '{"error": "not found"}'
assert simple_response.status_code == 404
content_type = request_adapter.get_response_content_type(simple_response)
async def test_get_root_parse_node(request_adapter, simple_success_response):
assert simple_success_response.text == '{"message": "Success!"}'
assert simple_success_response.status_code == 200
content_type = request_adapter.get_response_content_type(simple_success_response)
assert content_type == 'application/json'

with pytest.raises(Exception) as e:
await request_adapter.get_root_parse_node(simple_response)
await request_adapter.get_root_parse_node(simple_success_response)


@pytest.mark.asyncio
async def test_throw_failed_responses_null_error_map(request_adapter, simple_response):
assert simple_response.text == '{"error": "not found"}'
assert simple_response.status_code == 404
content_type = request_adapter.get_response_content_type(simple_response)
async def test_does_not_throw_failed_responses_on_success(request_adapter, simple_success_response):
try:
assert simple_success_response.text == '{"message": "Success!"}'
assert simple_success_response.status_code == 200
content_type = request_adapter.get_response_content_type(simple_success_response)
assert content_type == 'application/json'
except APIError as e:
assert False, f"'Function raised an exception {e}"


@pytest.mark.asyncio
async def test_throw_failed_responses_null_error_map(request_adapter, simple_error_response):
assert simple_error_response.text == '{"error": "not found"}'
assert simple_error_response.status_code == 404
content_type = request_adapter.get_response_content_type(simple_error_response)
assert content_type == 'application/json'

with pytest.raises(APIError) as e:
await request_adapter.throw_failed_responses(simple_response, None)
await request_adapter.throw_failed_responses(simple_error_response, None)
assert str(e.value) == "The server returned an unexpected status code and"\
" no error class is registered for this code 404"
assert e.value.response_status_code == 404


@pytest.mark.asyncio
async def test_throw_failed_responses_no_error_class(
request_adapter, simple_response, mock_error_map
request_adapter, simple_error_response, mock_error_map
):
assert simple_response.text == '{"error": "not found"}'
assert simple_response.status_code == 404
content_type = request_adapter.get_response_content_type(simple_response)
assert simple_error_response.text == '{"error": "not found"}'
assert simple_error_response.status_code == 404
content_type = request_adapter.get_response_content_type(simple_error_response)
assert content_type == 'application/json'

with pytest.raises(APIError) as e:
await request_adapter.throw_failed_responses(simple_response, mock_error_map)
await request_adapter.throw_failed_responses(simple_error_response, mock_error_map)
assert str(e.value) == "The server returned an unexpected status code and"\
" no error class is registered for this code 404"
assert e.value.response_status_code == 404


@pytest.mark.asyncio
async def test_throw_failed_responses_status_code_str_in_error_map(
async def test_throw_failed_responses_not_apierror(
request_adapter, mock_error_map, mock_error_object
):
request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object)
Expand All @@ -135,7 +146,20 @@ async def test_throw_failed_responses_status_code_str_in_error_map(

with pytest.raises(Exception) as e:
await request_adapter.throw_failed_responses(resp, mock_error_map)
assert str(e.value) == "Internal Server Error"
assert str(e.value) == "Unexpected error type: <class 'Exception'>"


@pytest.mark.asyncio
async def test_throw_failed_responses(request_adapter, mock_apierror_map, mock_error_object):
request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object)
resp = httpx.Response(status_code=500, headers={"Content-Type": "application/json"})
assert resp.status_code == 500
content_type = request_adapter.get_response_content_type(resp)
assert content_type == 'application/json'

with pytest.raises(APIError) as e:
await request_adapter.throw_failed_responses(resp, mock_apierror_map)
assert str(e.value) == "Custom Internal Server Error"


@pytest.mark.asyncio
Expand Down

0 comments on commit 01b6aa0

Please sign in to comment.