Skip to content

Commit 3ee9789

Browse files
committed
rebase
1 parent 00e3f81 commit 3ee9789

File tree

2 files changed

+70
-20
lines changed

2 files changed

+70
-20
lines changed

nylas/handler/http_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,11 @@ def _execute(
9191
timeout = overrides["timeout"]
9292

9393
# Serialize request_body to JSON with ensure_ascii=False to preserve UTF-8 characters
94-
# This ensures special characters (accented letters, emoji, etc.) are not escaped
94+
# (special characters, emoji, accented letters, etc.) and encode as UTF-8 bytes
95+
# to avoid Latin-1 encoding errors in the HTTP layer
9596
json_data = None
9697
if request_body is not None and data is None:
97-
json_data = json.dumps(request_body, ensure_ascii=False)
98+
json_data = json.dumps(request_body, ensure_ascii=False).encode('utf-8')
9899
try:
99100
response = requests.request(
100101
request["method"],

tests/handler/test_http_client.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def test_execute(self, http_client, patched_version_and_sys, patched_request):
302302
"Content-type": "application/json; charset=utf-8",
303303
"test": "header",
304304
},
305-
data='{"foo": "bar"}',
305+
data=b'{"foo": "bar"}',
306306
timeout=30,
307307
)
308308

@@ -336,7 +336,7 @@ def test_execute_override_timeout(
336336
"Content-type": "application/json; charset=utf-8",
337337
"test": "header",
338338
},
339-
data='{"foo": "bar"}',
339+
data=b'{"foo": "bar"}',
340340
timeout=60,
341341
)
342342

@@ -426,12 +426,12 @@ def test_execute_with_headers(self, http_client, patched_version_and_sys, patche
426426
"Content-type": "application/json; charset=utf-8",
427427
"test": "header",
428428
},
429-
data='{"foo": "bar"}',
429+
data=b'{"foo": "bar"}',
430430
timeout=30,
431431
)
432432

433433
def test_execute_with_utf8_characters(self, http_client, patched_version_and_sys, patched_request):
434-
"""Test that UTF-8 characters are preserved in JSON requests (not escaped)."""
434+
"""Test that UTF-8 characters are safely encoded in JSON requests."""
435435
mock_response = Mock()
436436
mock_response.json.return_value = {"success": True}
437437
mock_response.headers = {"X-Test-Header": "test"}
@@ -452,17 +452,21 @@ def test_execute_with_utf8_characters(self, http_client, patched_version_and_sys
452452
)
453453

454454
assert response_json == {"success": True}
455-
# Verify that the data sent preserves UTF-8 characters (not escaped)
455+
# Verify that the data is sent as UTF-8 encoded bytes
456456
call_kwargs = patched_request.call_args[1]
457457
assert "data" in call_kwargs
458458
sent_data = call_kwargs["data"]
459459

460-
# The JSON should contain actual UTF-8 characters, not escape sequences
461-
assert "Réunion d'équipe" in sent_data
462-
assert "De l'idée à la post-prod" in sent_data
463-
assert "café" in sent_data
460+
# Data should be bytes
461+
assert isinstance(sent_data, bytes)
462+
463+
# The JSON should contain actual UTF-8 characters (not escaped)
464+
decoded = sent_data.decode('utf-8')
465+
assert "Réunion d'équipe" in decoded
466+
assert "De l'idée à la post-prod" in decoded
467+
assert "café" in decoded
464468
# Should NOT contain unicode escape sequences
465-
assert "\\u" not in sent_data
469+
assert "\\u" not in decoded
466470

467471
def test_execute_with_none_request_body(self, http_client, patched_version_and_sys, patched_request):
468472
"""Test that None request_body is handled correctly."""
@@ -485,7 +489,7 @@ def test_execute_with_none_request_body(self, http_client, patched_version_and_s
485489
assert call_kwargs["data"] is None
486490

487491
def test_execute_with_emoji_and_international_characters(self, http_client, patched_version_and_sys, patched_request):
488-
"""Test that emoji and various international characters are preserved."""
492+
"""Test that emoji and various international characters are safely encoded."""
489493
mock_response = Mock()
490494
mock_response.json.return_value = {"success": True}
491495
mock_response.headers = {"X-Test-Header": "test"}
@@ -511,13 +515,58 @@ def test_execute_with_emoji_and_international_characters(self, http_client, patc
511515
call_kwargs = patched_request.call_args[1]
512516
sent_data = call_kwargs["data"]
513517

514-
# All characters should be preserved
515-
assert "🎉 Party time! 🥳" in sent_data
516-
assert "こんにちは" in sent_data
517-
assert "你好" in sent_data
518-
assert "Привет" in sent_data
519-
assert "Größe" in sent_data
520-
assert "¿Cómo estás?" in sent_data
518+
# Data should be bytes
519+
assert isinstance(sent_data, bytes)
520+
521+
# All characters should be preserved (not escaped)
522+
decoded = sent_data.decode('utf-8')
523+
assert "🎉 Party time! 🥳" in decoded
524+
assert "こんにちは" in decoded
525+
assert "你好" in decoded
526+
assert "Привет" in decoded
527+
assert "Größe" in decoded
528+
assert "¿Cómo estás?" in decoded
529+
# Should NOT contain unicode escape sequences
530+
assert "\\u" not in decoded
531+
532+
def test_execute_with_right_single_quotation_mark(self, http_client, patched_version_and_sys, patched_request):
533+
"""Test that right single quotation mark (\\u2019) is handled correctly.
534+
535+
This character caused UnicodeEncodeError: 'latin-1' codec can't encode character '\\u2019'.
536+
"""
537+
mock_response = Mock()
538+
mock_response.json.return_value = {"success": True}
539+
mock_response.headers = {"X-Test-Header": "test"}
540+
mock_response.status_code = 200
541+
patched_request.return_value = mock_response
542+
543+
# The \u2019 character is the right single quotation mark (')
544+
# This was the exact character that caused the original encoding error
545+
request_body = {
546+
"subject": "It's a test", # Contains \u2019 (right single quotation mark)
547+
"body": "Here's another example with curly apostrophe",
548+
}
549+
550+
response_json, response_headers = http_client._execute(
551+
method="POST",
552+
path="/messages/send",
553+
request_body=request_body,
554+
)
555+
556+
assert response_json == {"success": True}
557+
call_kwargs = patched_request.call_args[1]
558+
sent_data = call_kwargs["data"]
559+
560+
# Data should be bytes
561+
assert isinstance(sent_data, bytes)
562+
563+
# The character should be preserved (not escaped)
564+
decoded = sent_data.decode('utf-8')
565+
assert "'" in decoded # \u2019 right single quotation mark
566+
assert "It's a test" in decoded
567+
assert "Here's another" in decoded
568+
# Should NOT contain unicode escape sequences
569+
assert "\\u2019" not in decoded
521570

522571
def test_execute_with_multipart_data_not_affected(self, http_client, patched_version_and_sys, patched_request):
523572
"""Test that multipart/form-data is not affected by the change."""

0 commit comments

Comments
 (0)