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())
|