Skip to content

Commit d887401

Browse files
committed
fix: sanitize 502 HTML responses from error messages
This commit fixes an issue where 502 Bad Gateway responses from upstream proxies (returning raw HTML) would cause unreadable exceptions. The error handling now attempts to parse JSON and falls back to a clean status message if the response body is HTML. Includes unit tests.
1 parent 56a09e4 commit d887401

File tree

2 files changed

+88
-4
lines changed

2 files changed

+88
-4
lines changed

src/lingodotdev/engine.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,17 @@ async def _localize_chunk(
185185

186186
if not response.is_success:
187187
if 500 <= response.status_code < 600:
188+
error_details = ""
189+
try:
190+
error_json = response.json()
191+
if isinstance(error_json, dict) and "error" in error_json:
192+
error_details = f" {error_json['error']}"
193+
except Exception:
194+
pass
195+
188196
raise RuntimeError(
189-
f"Server error ({response.status_code}): {response.reason_phrase}. "
190-
f"{response.text}. This may be due to temporary service issues."
197+
f"Server error ({response.status_code}): {response.reason_phrase}.{error_details} "
198+
"This may be due to temporary service issues."
191199
)
192200
elif response.status_code == 400:
193201
raise ValueError(
@@ -427,8 +435,16 @@ async def recognize_locale(self, text: str) -> str:
427435

428436
if not response.is_success:
429437
if 500 <= response.status_code < 600:
438+
error_details = ""
439+
try:
440+
error_json = response.json()
441+
if isinstance(error_json, dict) and "error" in error_json:
442+
error_details = f" {error_json['error']}"
443+
except Exception:
444+
pass
445+
430446
raise RuntimeError(
431-
f"Server error ({response.status_code}): {response.reason_phrase}. "
447+
f"Server error ({response.status_code}): {response.reason_phrase}.{error_details} "
432448
"This may be due to temporary service issues."
433449
)
434450
raise RuntimeError(
@@ -461,8 +477,16 @@ async def whoami(self) -> Optional[Dict[str, str]]:
461477
return {"email": payload["email"], "id": payload["id"]}
462478

463479
if 500 <= response.status_code < 600:
480+
error_details = ""
481+
try:
482+
error_json = response.json()
483+
if isinstance(error_json, dict) and "error" in error_json:
484+
error_details = f" {error_json['error']}"
485+
except Exception:
486+
pass
487+
464488
raise RuntimeError(
465-
f"Server error ({response.status_code}): {response.reason_phrase}. "
489+
f"Server error ({response.status_code}): {response.reason_phrase}.{error_details} "
466490
"This may be due to temporary service issues."
467491
)
468492

tests/test_502_handling.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import pytest
2+
from unittest.mock import Mock, patch
3+
from lingodotdev import LingoDotDevEngine
4+
5+
6+
@pytest.mark.asyncio
7+
async def test_502_html_handling():
8+
"""Test that 502 errors with HTML bodies are sanitized"""
9+
config = {"api_key": "test_key", "api_url": "https://api.test.com"}
10+
engine = LingoDotDevEngine(config)
11+
12+
html_body = """<html><body><h1>502 Bad Gateway</h1></body></html>"""
13+
14+
with patch("lingodotdev.engine.httpx.AsyncClient.post") as mock_post:
15+
mock_response = Mock()
16+
mock_response.is_success = False
17+
mock_response.status_code = 502
18+
mock_response.reason_phrase = "Bad Gateway"
19+
mock_response.text = html_body
20+
mock_response.json.side_effect = ValueError(
21+
"Not JSON"
22+
) # simulating non-JSON response
23+
mock_post.return_value = mock_response
24+
25+
with pytest.raises(RuntimeError) as exc_info:
26+
await engine.localize_text("hello", {"target_locale": "es"})
27+
28+
error_msg = str(exc_info.value)
29+
30+
# Assertions
31+
assert "Server error (502): Bad Gateway." in error_msg
32+
assert "This may be due to temporary service issues." in error_msg
33+
assert "<html>" not in error_msg
34+
assert "<body>" not in error_msg
35+
36+
37+
@pytest.mark.asyncio
38+
async def test_500_json_handling():
39+
"""Test that 500 errors with JSON bodies are preserved"""
40+
config = {"api_key": "test_key", "api_url": "https://api.test.com"}
41+
engine = LingoDotDevEngine(config)
42+
43+
error_json = {"error": "Specific internal error message"}
44+
45+
with patch("lingodotdev.engine.httpx.AsyncClient.post") as mock_post:
46+
mock_response = Mock()
47+
mock_response.is_success = False
48+
mock_response.status_code = 500
49+
mock_response.reason_phrase = "Internal Server Error"
50+
mock_response.json.return_value = error_json
51+
mock_post.return_value = mock_response
52+
53+
with pytest.raises(RuntimeError) as exc_info:
54+
await engine.localize_text("hello", {"target_locale": "es"})
55+
56+
error_msg = str(exc_info.value)
57+
58+
# Assertions
59+
assert "Server error (500): Internal Server Error." in error_msg
60+
assert "Specific internal error message" in error_msg

0 commit comments

Comments
 (0)