Skip to content
Merged
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
16 changes: 14 additions & 2 deletions dropbox/dropbox_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from stone.backends.python_rsrc import stone_serializers

from dropbox import files
from dropbox.auth import AuthError as AuthErrorUnion
from dropbox.auth import (
AuthError_validator,
RateLimitError_validator,
Expand Down Expand Up @@ -726,8 +727,19 @@ def raise_dropbox_error_for_resp(self, res):
"Expected content-type to be application/json, got %r"
% res.headers.get("content-type")
)
err = stone_serializers.json_compat_obj_decode(AuthError_validator, res.json()["error"])
raise AuthError(request_id, err)
raw_error = res.json()["error"]
# Stone reserves the catch-all tag for unknown variants and rejects
# it on the wire, even with strict=False. Some 401 responses send
# "other" explicitly; keep those as authentication errors too.
if raw_error == "other" or (
isinstance(raw_error, dict) and raw_error.get(".tag") == "other"
):
err = AuthErrorUnion.other
else:
err = stone_serializers.json_compat_obj_decode(
AuthError_validator, raw_error, strict=False
)
raise AuthError(request_id, err, body=res.text)
elif res.status_code == HTTP_STATUS_INVALID_PATH_ROOT:
err = stone_serializers.json_compat_obj_decode(
PathRootError_validator, res.json()["error"]
Expand Down
9 changes: 6 additions & 3 deletions dropbox/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@ def __repr__(self):


class AuthError(HttpError):
"""Errors due to invalid authentication credentials."""
"""Errors due to invalid authentication credentials.

def __init__(self, request_id, error):
super(AuthError, self).__init__(request_id, 401, None)
The optional ``body`` contains the original HTTP response body, when available.
"""

def __init__(self, request_id, error, body=None):
super(AuthError, self).__init__(request_id, 401, body)
self.error = error

def __repr__(self):
Expand Down
45 changes: 45 additions & 0 deletions test/unit/test_dropbox_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,51 @@ def test_refresh_raises_after_exhausting_retries(self, server_error_session_inst
# Initial attempt + 2 retries.
assert server_error_session_instance.post.call_count == 3

@pytest.mark.parametrize(
"raw_error, expected_tag",
[
("other", "other"),
({".tag": "other"}, "other"),
("future_auth_error", "other"),
({".tag": "future_auth_error", "details": "new server error"}, "other"),
("invalid_access_token", "invalid_access_token"),
({".tag": "invalid_access_token", "future_field": True}, "invalid_access_token"),
(
{
".tag": "missing_scope",
"required_scope": "files.content.write",
"future_field": True,
},
"missing_scope",
),
],
)
def test_upload_preserves_auth_errors(self, session_instance, mocker, raw_error, expected_tag):
response = requests.Response()
response.status_code = 401
response.headers = {
"content-type": "application/json",
"x-dropbox-request-id": "auth-request-id",
}
response._content = json.dumps(
{"error": raw_error, "error_summary": "authentication failed"}
).encode("utf-8")
session_instance.post.return_value = response
dbx = Dropbox(ACCESS_TOKEN, session=session_instance)
refresh = mocker.patch.object(dbx, "refresh_access_token")

with pytest.raises(AuthError) as caught:
dbx.files_upload(b"contents", "/test.txt")

assert getattr(caught.value.error, "is_" + expected_tag)()
if expected_tag == "missing_scope":
assert caught.value.error.get_missing_scope().required_scope == "files.content.write"
assert caught.value.status_code == 401
assert caught.value.request_id == "auth-request-id"
assert caught.value.body == response.text
session_instance.post.assert_called_once()
refresh.assert_not_called()

def test_refresh_on_expired_access_token_uses_client_scope(self, session_instance, mocker):
# A 401 expired_access_token triggers a refresh, which must request the
# scope the client was configured with, like check_and_refresh_access_token.
Expand Down
Loading