Skip to content

Commit d8fc17c

Browse files
Decode global access errors on HTTP 403
1 parent eee36cb commit d8fc17c

2 files changed

Lines changed: 81 additions & 3 deletions

File tree

dropbox/dropbox_client.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from datetime import datetime, timedelta
2222
from dropbox.auth import (
23+
AccessError_validator,
2324
AuthError_validator,
2425
RateLimitError_validator,
2526
)
@@ -51,7 +52,7 @@
5152
pinned_session,
5253
DEFAULT_TIMEOUT
5354
)
54-
from stone.backends.python_rsrc import stone_serializers
55+
from stone.backends.python_rsrc import stone_serializers, stone_validators
5556

5657
PATH_ROOT_HEADER = 'Dropbox-API-Path-Root'
5758
HTTP_STATUS_INVALID_PATH_ROOT = 422
@@ -698,7 +699,28 @@ def raise_dropbox_error_for_resp(self, res):
698699
else:
699700
retry_after = None
700701
raise RateLimitError(request_id, err, retry_after)
701-
elif res.status_code in (403, 404, 409):
702+
elif res.status_code == 403:
703+
# Access errors are global API errors rather than errors from the
704+
# individual route. Decode strictly so a route-specific error that
705+
# happens to use HTTP 403 can still be handled by the requester.
706+
try:
707+
body = res.json()
708+
err = stone_serializers.json_compat_obj_decode(
709+
AccessError_validator, body['error'], strict=True)
710+
except (ValueError, TypeError, KeyError,
711+
stone_validators.ValidationError):
712+
return
713+
714+
user_message = body.get('user_message')
715+
if isinstance(user_message, dict):
716+
user_message_text = user_message.get('text')
717+
user_message_locale = user_message.get('locale')
718+
else:
719+
user_message_text = None
720+
user_message_locale = None
721+
raise ApiError(request_id, err,
722+
user_message_text, user_message_locale)
723+
elif res.status_code in (404, 409):
702724
# special case handled by requester
703725
return
704726
elif not (200 <= res.status_code <= 299):

test/unit/test_dropbox_unit.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
)
1616
from dropbox.content_hash import content_hash
1717
from dropbox.common import PathRoot
18-
from dropbox.exceptions import AuthError, BadInputError
18+
from dropbox.exceptions import ApiError, AuthError, BadInputError
1919
from dropbox.oauth import OAuth2FlowNoRedirectResult, DropboxOAuth2FlowNoRedirect
2020
from datetime import datetime, timedelta
2121

@@ -509,6 +509,62 @@ def test_refresh_raises_after_exhausting_retries(self, server_error_session_inst
509509
# Initial attempt + 2 retries.
510510
assert server_error_session_instance.post.call_count == 3
511511

512+
def test_upload_decodes_global_access_error(self, mocker):
513+
payload = {
514+
'error': {
515+
'.tag': 'invalid_account_type',
516+
'invalid_account_type': {'.tag': 'feature'},
517+
},
518+
'error_summary': 'invalid_account_type/feature/',
519+
'user_message': {
520+
'text': 'Uploads are unavailable for this account.',
521+
'locale': 'en',
522+
},
523+
}
524+
response = mock.MagicMock(status_code=403)
525+
response.headers = {
526+
'content-type': 'application/json',
527+
'x-dropbox-request-id': 'request-id',
528+
}
529+
response.json.return_value = payload
530+
response.content = json.dumps(payload).encode('utf-8')
531+
response.text = json.dumps(payload)
532+
session_obj = create_session()
533+
mocker.patch.object(session_obj, 'post', return_value=response)
534+
dbx = Dropbox(oauth2_access_token=ACCESS_TOKEN, session=session_obj)
535+
536+
with pytest.raises(ApiError) as exc_info:
537+
dbx.files_upload(b'test', '/test.txt')
538+
539+
assert exc_info.value.request_id == 'request-id'
540+
assert exc_info.value.error.is_invalid_account_type()
541+
assert exc_info.value.error.get_invalid_account_type().is_feature()
542+
assert exc_info.value.user_message_text == \
543+
'Uploads are unavailable for this account.'
544+
assert exc_info.value.user_message_locale == 'en'
545+
546+
def test_upload_preserves_route_specific_403_error(self, mocker):
547+
payload = {
548+
'error': {'.tag': 'payload_too_large'},
549+
'error_summary': 'payload_too_large/',
550+
}
551+
response = mock.MagicMock(status_code=403)
552+
response.headers = {
553+
'content-type': 'application/json',
554+
'x-dropbox-request-id': 'request-id',
555+
}
556+
response.json.return_value = payload
557+
response.content = json.dumps(payload).encode('utf-8')
558+
response.text = json.dumps(payload)
559+
session_obj = create_session()
560+
mocker.patch.object(session_obj, 'post', return_value=response)
561+
dbx = Dropbox(oauth2_access_token=ACCESS_TOKEN, session=session_obj)
562+
563+
with pytest.raises(ApiError) as exc_info:
564+
dbx.files_upload(b'test', '/test.txt')
565+
566+
assert exc_info.value.error.is_payload_too_large()
567+
512568

513569
class TestAutoContentHash:
514570

0 commit comments

Comments
 (0)