diff --git a/src/grimoire/services/backup_s3.py b/src/grimoire/services/backup_s3.py index 63324f1a..cf54bc32 100644 --- a/src/grimoire/services/backup_s3.py +++ b/src/grimoire/services/backup_s3.py @@ -120,6 +120,12 @@ def _says(response: httpx.Response) -> str: "AccessDenied": "the key isn't allowed to do that", } said = hints.get(code) or message or response.reason_phrase + if not code and 400 <= response.status_code < 500: + # S3 refuses with an body. A 4xx without one came from + # something else at that address — most often a web server on the + # default port, when the S3 service listens on a port of its own. + said = ("the address answered, but not as S3 — check the scheme and " + "the port (an S3 service usually listens on its own port)") return f"HTTP {response.status_code}{f' {code}' if code else ''}: {said}." diff --git a/tests/test_services_backup_s3.py b/tests/test_services_backup_s3.py index be2d7d59..ddca269a 100644 --- a/tests/test_services_backup_s3.py +++ b/tests/test_services_backup_s3.py @@ -78,6 +78,22 @@ def test_a_refusal_says_why(tmp_path): asyncio.run(s3.put_file(TARGET, "b.tar", f, transport=transport)) +def test_a_web_server_at_the_address_is_named_as_not_s3(): + """No port in the address: the request lands on whatever serves port 80, + and its plain 404 says nothing about S3.""" + page = b"

404 Not Found

" + transport = httpx.MockTransport(lambda r: httpx.Response(404, content=page)) + with pytest.raises(s3.S3Error, match=r"HTTP 404: the address answered, but not as S3 — check the scheme and the port"): + asyncio.run(s3.list_objects(TARGET, transport=transport)) + + +def test_a_server_error_without_a_body_is_not_blamed_on_the_port(): + transport = httpx.MockTransport(lambda r: httpx.Response(503)) + with pytest.raises(s3.S3Error, match="HTTP 503: Service Unavailable") as err: + asyncio.run(s3.list_objects(TARGET, transport=transport)) + assert "port" not in str(err.value) + + def test_unreachable_says_so(tmp_path): f = tmp_path / "b.tar" f.write_bytes(b"x")