File: test_utils_errors.py

package info (click to toggle)
huggingface-hub 1.2.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,424 kB
  • sloc: python: 45,857; sh: 434; makefile: 33
file content (328 lines) | stat: -rw-r--r-- 17,143 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
import unittest
from unittest.mock import Mock

import pytest
from httpx import Request, Response

from huggingface_hub.errors import (
    BadRequestError,
    DisabledRepoError,
    EntryNotFoundError,
    HfHubHTTPError,
    RepositoryNotFoundError,
    RevisionNotFoundError,
)
from huggingface_hub.utils._http import REPO_API_REGEX, X_AMZN_TRACE_ID, X_REQUEST_ID, _format, hf_raise_for_status


class TestErrorUtils(unittest.TestCase):
    def test_hf_raise_for_status_repo_not_found(self) -> None:
        response = Response(status_code=404, headers={"X-Error-Code": "RepoNotFound", X_REQUEST_ID: "123"})
        response.request = Request(method="GET", url="https://huggingface.co/fake")
        with self.assertRaisesRegex(RepositoryNotFoundError, "Repository Not Found") as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 404
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_disabled_repo(self) -> None:
        response = Response(
            status_code=403, headers={"X-Error-Message": "Access to this resource is disabled.", X_REQUEST_ID: "123"}
        )
        response.request = Request(method="GET", url="https://huggingface.co/fake")

        with self.assertRaises(DisabledRepoError) as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 403
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_401_repo_url_not_invalid_token(self) -> None:
        response = Response(status_code=401, headers={X_REQUEST_ID: "123"})
        response.request = Request(method="GET", url="https://huggingface.co/api/models/username/reponame")
        with self.assertRaisesRegex(RepositoryNotFoundError, "Repository Not Found") as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 401
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_401_repo_url_invalid_token(self) -> None:
        response = Response(
            status_code=401,
            headers={X_REQUEST_ID: "123", "X-Error-Message": "Invalid credentials in Authorization header"},
        )
        response.request = Request(method="GET", url="https://huggingface.co/api/models/username/reponame")
        with self.assertRaisesRegex(HfHubHTTPError, "Invalid credentials in Authorization header") as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 401
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_403_wrong_token_scope(self) -> None:
        response = Response(
            status_code=403, headers={X_REQUEST_ID: "123", "X-Error-Message": "specific error message"}
        )
        response.request = Request(method="GET", url="https://huggingface.co/api/repos/create")
        expected_message_part = "403 Forbidden: specific error message"
        with self.assertRaisesRegex(HfHubHTTPError, expected_message_part) as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 403
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_401_not_repo_url(self) -> None:
        response = Response(status_code=401, headers={X_REQUEST_ID: "123"})
        response.request = Request(method="GET", url="https://huggingface.co/api/collections")
        with self.assertRaises(HfHubHTTPError) as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 401
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_revision_not_found(self) -> None:
        response = Response(status_code=404, headers={"X-Error-Code": "RevisionNotFound", X_REQUEST_ID: "123"})
        response.request = Request(method="GET", url="https://huggingface.co/fake")
        with self.assertRaisesRegex(RevisionNotFoundError, "Revision Not Found") as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 404
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_entry_not_found(self) -> None:
        response = Response(status_code=404, headers={"X-Error-Code": "EntryNotFound", X_REQUEST_ID: "123"})
        response.request = Request(method="GET", url="https://huggingface.co/fake")
        with self.assertRaisesRegex(EntryNotFoundError, "Entry Not Found") as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 404
        assert "Request ID: 123" in str(context.exception)

    def test_hf_raise_for_status_bad_request_no_endpoint_name(self) -> None:
        """Test HTTPError converted to BadRequestError if error 400."""
        response = Response(status_code=400)
        response.request = Request(method="GET", url="https://huggingface.co/fake")
        with self.assertRaisesRegex(BadRequestError, "Bad request:") as context:
            hf_raise_for_status(response)
        assert context.exception.response.status_code == 400

    def test_hf_raise_for_status_bad_request_with_endpoint_name(self) -> None:
        """Test endpoint name is added to BadRequestError message."""
        response = Response(status_code=400)
        response.request = Request(method="GET", url="https://huggingface.co/fake")
        with self.assertRaisesRegex(BadRequestError, "Bad request for preupload endpoint:") as context:
            hf_raise_for_status(response, endpoint_name="preupload")
        assert context.exception.response.status_code == 400

    def test_hf_raise_for_status_fallback(self) -> None:
        """Test HTTPError is converted to HfHubHTTPError."""
        response = Response(status_code=404, headers={X_REQUEST_ID: "test-id"})
        response.request = Request(method="GET", url="https://huggingface.co/fake")
        with self.assertRaisesRegex(HfHubHTTPError, "Request ID: test-id") as context:
            hf_raise_for_status(response)

        assert context.exception.response.status_code == 404
        assert context.exception.response.url == "https://huggingface.co/fake"


class TestHfHubHTTPError(unittest.TestCase):
    response: Response

    def setUp(self) -> None:
        """Setup with a default response."""
        self.response = Response(status_code=404, request=Request(method="GET", url="https://huggingface.co/fake"))

    def test_hf_hub_http_error_initialization(self) -> None:
        """Test HfHubHTTPError is initialized properly."""
        error = HfHubHTTPError("this is a message", response=self.response)
        assert str(error) == "this is a message"
        assert error.response == self.response
        assert error.request_id is None
        assert error.server_message is None

    def test_hf_hub_http_error_init_with_request_id(self) -> None:
        """Test request id is added to the message."""
        self.response.headers = {X_REQUEST_ID: "test-id"}
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message (Request ID: test-id)"
        assert error.request_id == "test-id"

    def test_hf_hub_http_error_init_with_request_id_and_multiline_message(self) -> None:
        """Test request id is added to the end of the first line."""
        self.response.headers = {X_REQUEST_ID: "test-id"}
        error = _format(HfHubHTTPError, "this is a message\nthis is more details", response=self.response)
        assert str(error) == "this is a message (Request ID: test-id)\nthis is more details"

        error = _format(HfHubHTTPError, "this is a message\n\nthis is more details", response=self.response)
        assert str(error) == "this is a message (Request ID: test-id)\n\nthis is more details"

    def test_hf_hub_http_error_init_with_request_id_already_in_message(self) -> None:
        """Test request id is not duplicated in error message (case-insensitive)"""
        self.response.headers = {X_REQUEST_ID: "test-id"}
        error = _format(HfHubHTTPError, "this is a message on request TEST-ID", response=self.response)
        assert str(error) == "this is a message on request TEST-ID"
        assert error.request_id == "test-id"

    def test_hf_hub_http_error_init_with_server_error(self) -> None:
        """Test server error is added to the error message."""
        self.response._content = b'{"error": "This is a message returned by the server"}'
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message\n\nThis is a message returned by the server"
        assert error.server_message == "This is a message returned by the server"

    def test_hf_hub_http_error_init_with_server_error_and_multiline_message(
        self,
    ) -> None:
        """Test server error is added to the error message after the details."""
        self.response._content = b'{"error": "This is a message returned by the server"}'
        error = _format(HfHubHTTPError, "this is a message\n\nSome details.", response=self.response)
        assert str(error) == "this is a message\n\nSome details.\nThis is a message returned by the server"

    def test_hf_hub_http_error_init_with_multiple_server_errors(
        self,
    ) -> None:
        """Test server errors are added to the error message after the details.

        Regression test for https://github.com/huggingface/huggingface_hub/issues/1114.
        """
        self.response._content = (
            b'{"httpStatusCode": 400, "errors": [{"message": "this is error 1", "type":'
            b' "error"}, {"message": "this is error 2", "type": "error"}]}'
        )
        error = _format(HfHubHTTPError, "this is a message\n\nSome details.", response=self.response)
        assert str(error) == "this is a message\n\nSome details.\nthis is error 1\nthis is error 2"

    def test_hf_hub_http_error_init_with_server_error_already_in_message(
        self,
    ) -> None:
        """Test server error is not duplicated if already in details.

        Case-insensitive.
        """
        self.response._content = b'{"error": "repo NOT found"}'
        error = _format(
            HfHubHTTPError,
            "this is a message\n\nRepo Not Found. and more\nand more",
            response=self.response,
        )
        assert str(error) == "this is a message\n\nRepo Not Found. and more\nand more"

    def test_hf_hub_http_error_init_with_unparsable_server_error(
        self,
    ) -> None:
        """Server returned a text message (not as JSON) => should be added to the exception."""
        self.response._content = b"this is not a json-formatted string"
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message\n\nthis is not a json-formatted string"
        assert error.server_message == "this is not a json-formatted string"

    def test_hf_hub_http_error_append_to_message(self) -> None:
        """Test add extra information to existing HfHubHTTPError."""
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        error.args = error.args + (1, 2, 3)  # faking some extra args

        error.append_to_message("\nthis is an additional message")
        assert error.args == ("this is a message\nthis is an additional message", 1, 2, 3)

        assert error.server_message is None  # added message is not from server

    def test_hf_hub_http_error_init_with_error_message_in_header(self) -> None:
        """Test server error from header is added to the error message."""
        self.response.headers = {"X-Error-Message": "Error message from headers."}
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message\n\nError message from headers."
        assert error.server_message == "Error message from headers."

    def test_hf_hub_http_error_init_with_error_message_from_header_and_body(
        self,
    ) -> None:
        """Test server error from header and from body are added to the error message."""
        self.response._content = b'{"error": "Error message from body."}'
        self.response.headers = {"X-Error-Message": "Error message from headers."}
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message\n\nError message from headers.\nError message from body."
        assert error.server_message == "Error message from headers.\nError message from body."

    def test_hf_hub_http_error_init_with_error_message_duplicated_in_header_and_body(
        self,
    ) -> None:
        """Test server error from header and from body are the same.

        Should not duplicate it in the raised `HfHubHTTPError`.
        """
        self.response._content = b'{"error": "Error message duplicated in headers and body."}'
        self.response.headers = {"X-Error-Message": "Error message duplicated in headers and body."}
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message\n\nError message duplicated in headers and body."
        assert error.server_message == "Error message duplicated in headers and body."

    def test_hf_hub_http_error_without_request_id_with_amzn_trace_id(self) -> None:
        """Test request id is not duplicated in error message (case-insensitive)"""
        self.response.headers = {X_AMZN_TRACE_ID: "test-trace-id"}
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message (Amzn Trace ID: test-trace-id)"
        assert error.request_id == "test-trace-id"

    def test_hf_hub_http_error_with_request_id_and_amzn_trace_id(self) -> None:
        """Test request id is not duplicated in error message (case-insensitive)"""
        self.response.headers = {X_AMZN_TRACE_ID: "test-trace-id", X_REQUEST_ID: "test-id"}
        error = _format(HfHubHTTPError, "this is a message", response=self.response)
        assert str(error) == "this is a message (Request ID: test-id)"
        assert error.request_id == "test-id"

    def test_hf_hub_error_reconstruction(self) -> None:
        """Test HfHubHTTPError is reconstructed properly."""
        from copy import deepcopy

        mock_response = Response(status_code=404, request=Request(method="GET", url="https://huggingface.co/fake"))
        error = HfHubHTTPError("this is a message", response=mock_response)
        copy_error = deepcopy(error)
        assert str(copy_error) == str(error)
        assert copy_error.request_id == error.request_id
        assert copy_error.server_message == error.server_message


@pytest.mark.parametrize(
    ("url", "should_match"),
    [
        # Listing endpoints => False
        ("https://huggingface.co/api/models", False),
        ("https://huggingface.co/api/datasets", False),
        ("https://huggingface.co/api/spaces", False),
        # Create repo endpoint => False
        ("https://huggingface.co/api/repos/create", False),
        # Collection endpoints => False
        ("https://huggingface.co/api/collections", False),
        ("https://huggingface.co/api/collections/foo/bar", False),
        # Repo endpoints => True
        ("https://huggingface.co/api/models/repo_id", True),
        ("https://huggingface.co/api/datasets/repo_id", True),
        ("https://huggingface.co/api/spaces/repo_id", True),
        ("https://huggingface.co/api/models/username/repo_name/refs/main", True),
        ("https://huggingface.co/api/datasets/username/repo_name/refs/main", True),
        ("https://huggingface.co/api/spaces/username/repo_name/refs/main", True),
        # Inference Endpoint => False
        ("https://api.endpoints.huggingface.cloud/v2/endpoint/namespace", False),
        # Staging Endpoint => True
        ("https://hub-ci.huggingface.co/api/models/repo_id", True),
        ("https://hub-ci.huggingface.co/api/datasets/repo_id", True),
        ("https://hub-ci.huggingface.co/api/spaces/repo_id", True),
        # /resolve Endpoint => True
        ("https://huggingface.co/gpt2/resolve/main/README.md", True),
        ("https://huggingface.co/datasets/google/fleurs/resolve/revision/README.md", True),
        # Regression tests
        ("https://huggingface.co/bert-base/resolve/main/pytorch_model.bin", True),
        ("https://hub-ci.huggingface.co/__DUMMY_USER__/repo-1470b5/resolve/main/file.txt", True),
    ],
)
def test_repo_api_regex(url: str, should_match: bool) -> None:
    """Test the regex used to match repo API URLs."""
    if should_match:
        assert REPO_API_REGEX.match(url)
    else:
        assert REPO_API_REGEX.match(url) is None


def test_hf_hub_http_error_inherits_from_os_error() -> None:
    """Test HfHubHTTPError inherits from OSError."""
    with pytest.raises(OSError):
        raise HfHubHTTPError("this is a message", response=Mock())