Skip to content

Commit 99957f3

Browse files
committed
fix: sanitize 502 HTML responses from error messages
1 parent 96095ab commit 99957f3

2 files changed

Lines changed: 95 additions & 7 deletions

File tree

src/lingodotdev/engine.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,18 @@ async def _localize_chunk(
217217
if not response.is_success:
218218
response_preview = self._truncate_response(response.text)
219219
if 500 <= response.status_code < 600:
220+
error_details = ""
221+
try:
222+
error_json = response.json()
223+
if isinstance(error_json, dict) and "error" in error_json:
224+
error_details = f" {error_json['error']}"
225+
except Exception:
226+
pass
227+
228+
raise RuntimeError(
220229
raise RuntimeError(
221-
f"Server error ({response.status_code}): {response.reason_phrase}. "
222-
f"This may be due to temporary service issues. Response: {response_preview}"
230+
f"Server error ({response.status_code}): {response.reason_phrase}.{error_details} "
231+
"This may be due to temporary service issues."
223232
)
224233
elif response.status_code == 400:
225234
raise ValueError(
@@ -463,9 +472,18 @@ async def recognize_locale(self, text: str) -> str:
463472
if not response.is_success:
464473
response_preview = self._truncate_response(response.text)
465474
if 500 <= response.status_code < 600:
475+
error_details = ""
476+
try:
477+
error_json = response.json()
478+
if isinstance(error_json, dict) and "error" in error_json:
479+
error_details = f" {error_json['error']}"
480+
except Exception:
481+
pass
482+
466483
raise RuntimeError(
467-
f"Server error ({response.status_code}): {response.reason_phrase}. "
468-
f"This may be due to temporary service issues. Response: {response_preview}"
484+
raise RuntimeError(
485+
f"Server error ({response.status_code}): {response.reason_phrase}.{error_details} "
486+
"This may be due to temporary service issues."
469487
)
470488
raise RuntimeError(
471489
f"Error recognizing locale ({response.status_code}): {response.reason_phrase}. "
@@ -498,10 +516,17 @@ async def whoami(self) -> Optional[Dict[str, str]]:
498516
return {"email": payload["email"], "id": payload["id"]}
499517

500518
if 500 <= response.status_code < 600:
501-
response_preview = self._truncate_response(response.text)
519+
error_details = ""
520+
try:
521+
error_json = response.json()
522+
if isinstance(error_json, dict) and "error" in error_json:
523+
error_details = f" {error_json['error']}"
524+
except Exception:
525+
pass
526+
502527
raise RuntimeError(
503-
f"Server error ({response.status_code}): {response.reason_phrase}. "
504-
f"This may be due to temporary service issues. Response: {response_preview}"
528+
f"Server error ({response.status_code}): {response.reason_phrase}.{error_details} "
529+
"This may be due to temporary service issues."
505530
)
506531

507532
return None

tests/test_502_handling.py

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

0 commit comments

Comments
 (0)