Skip to content

Commit f3178c2

Browse files
committed
more update
1 parent 3ee9789 commit f3178c2

File tree

2 files changed

+104
-54
lines changed

2 files changed

+104
-54
lines changed

β€Žnylas/handler/http_client.pyβ€Ž

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,25 @@ def _execute(
9090
if overrides and overrides.get("timeout"):
9191
timeout = overrides["timeout"]
9292

93-
# Serialize request_body to JSON with ensure_ascii=False to preserve UTF-8 characters
94-
# (special characters, emoji, accented letters, etc.) and encode as UTF-8 bytes
95-
# to avoid Latin-1 encoding errors in the HTTP layer
96-
json_data = None
97-
if request_body is not None and data is None:
98-
json_data = json.dumps(request_body, ensure_ascii=False).encode('utf-8')
93+
# Use requests' json parameter for proper UTF-8 handling when sending JSON
94+
# This avoids Latin-1 encoding errors with special characters (emoji, accented letters, etc.)
9995
try:
100-
response = requests.request(
101-
request["method"],
102-
request["url"],
103-
headers=request["headers"],
104-
data=json_data or data,
105-
timeout=timeout,
106-
)
96+
if request_body is not None and data is None:
97+
response = requests.request(
98+
request["method"],
99+
request["url"],
100+
headers=request["headers"],
101+
json=request_body,
102+
timeout=timeout,
103+
)
104+
else:
105+
response = requests.request(
106+
request["method"],
107+
request["url"],
108+
headers=request["headers"],
109+
data=data,
110+
timeout=timeout,
111+
)
107112
except requests.exceptions.Timeout as exc:
108113
raise NylasSdkTimeoutError(url=request["url"], timeout=timeout) from exc
109114

β€Žtests/handler/test_http_client.pyβ€Ž

Lines changed: 86 additions & 41 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=b'{"foo": "bar"}',
305+
json={"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=b'{"foo": "bar"}',
339+
json={"foo": "bar"},
340340
timeout=60,
341341
)
342342

@@ -426,7 +426,7 @@ 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=b'{"foo": "bar"}',
429+
json={"foo": "bar"},
430430
timeout=30,
431431
)
432432

@@ -452,21 +452,15 @@ 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 is sent as UTF-8 encoded bytes
455+
# Verify that the json parameter is used with original data
456456
call_kwargs = patched_request.call_args[1]
457-
assert "data" in call_kwargs
458-
sent_data = call_kwargs["data"]
459-
460-
# Data should be bytes
461-
assert isinstance(sent_data, bytes)
457+
assert "json" in call_kwargs
458+
sent_json = call_kwargs["json"]
462459

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
468-
# Should NOT contain unicode escape sequences
469-
assert "\\u" not in decoded
460+
# The JSON should contain actual UTF-8 characters
461+
assert sent_json["title"] == "RΓ©union d'Γ©quipe"
462+
assert sent_json["description"] == "De l'idΓ©e Γ  la post-prod, sans friction"
463+
assert sent_json["location"] == "cafΓ©"
470464

471465
def test_execute_with_none_request_body(self, http_client, patched_version_and_sys, patched_request):
472466
"""Test that None request_body is handled correctly."""
@@ -483,9 +477,33 @@ def test_execute_with_none_request_body(self, http_client, patched_version_and_s
483477
)
484478

485479
assert response_json == {"success": True}
486-
# Verify that data is None when request_body is None
480+
# Verify that data branch is used when request_body is None
487481
call_kwargs = patched_request.call_args[1]
482+
# Should use data= parameter, not json= parameter
488483
assert "data" in call_kwargs
484+
assert "json" not in call_kwargs
485+
assert call_kwargs["data"] is None
486+
487+
def test_execute_with_none_request_body_and_none_data(self, http_client, patched_version_and_sys, patched_request):
488+
"""Test that both None request_body and None data are handled correctly."""
489+
mock_response = Mock()
490+
mock_response.json.return_value = {"success": True}
491+
mock_response.headers = {"X-Test-Header": "test"}
492+
mock_response.status_code = 200
493+
patched_request.return_value = mock_response
494+
495+
response_json, response_headers = http_client._execute(
496+
method="DELETE",
497+
path="/events/123",
498+
request_body=None,
499+
data=None,
500+
)
501+
502+
assert response_json == {"success": True}
503+
call_kwargs = patched_request.call_args[1]
504+
# Should use data= parameter with None value
505+
assert "data" in call_kwargs
506+
assert "json" not in call_kwargs
489507
assert call_kwargs["data"] is None
490508

491509
def test_execute_with_emoji_and_international_characters(self, http_client, patched_version_and_sys, patched_request):
@@ -513,21 +531,15 @@ def test_execute_with_emoji_and_international_characters(self, http_client, patc
513531

514532
assert response_json == {"success": True}
515533
call_kwargs = patched_request.call_args[1]
516-
sent_data = call_kwargs["data"]
534+
sent_json = call_kwargs["json"]
517535

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
536+
# All characters should be preserved in the json dict
537+
assert sent_json["emoji"] == "πŸŽ‰ Party time! πŸ₯³"
538+
assert sent_json["japanese"] == "こんにけは"
539+
assert sent_json["chinese"] == "δ½ ε₯½"
540+
assert sent_json["russian"] == "ΠŸΡ€ΠΈΠ²Π΅Ρ‚"
541+
assert sent_json["german"] == "Grâße"
542+
assert sent_json["spanish"] == "ΒΏCΓ³mo estΓ‘s?"
531543

532544
def test_execute_with_right_single_quotation_mark(self, http_client, patched_version_and_sys, patched_request):
533545
"""Test that right single quotation mark (\\u2019) is handled correctly.
@@ -555,18 +567,51 @@ def test_execute_with_right_single_quotation_mark(self, http_client, patched_ver
555567

556568
assert response_json == {"success": True}
557569
call_kwargs = patched_request.call_args[1]
558-
sent_data = call_kwargs["data"]
570+
sent_json = call_kwargs["json"]
571+
572+
# The \u2019 character should be preserved
573+
assert "'" in sent_json["subject"] # \u2019 right single quotation mark
574+
assert sent_json["subject"] == "It's a test"
575+
assert "'" in sent_json["body"]
576+
assert "Here's another" in sent_json["body"]
577+
578+
def test_execute_with_emojis(self, http_client, patched_version_and_sys, patched_request):
579+
"""Test that emojis are handled correctly in request bodies.
559580
560-
# Data should be bytes
561-
assert isinstance(sent_data, bytes)
581+
Emojis are multi-byte UTF-8 characters that could cause encoding issues
582+
if not handled properly.
583+
"""
584+
mock_response = Mock()
585+
mock_response.json.return_value = {"success": True}
586+
mock_response.headers = {"X-Test-Header": "test"}
587+
mock_response.status_code = 200
588+
patched_request.return_value = mock_response
589+
590+
request_body = {
591+
"subject": "Hello πŸ‘‹ World 🌍",
592+
"body": "Great job! πŸŽ‰ Keep up the good work πŸ’ͺ See you soon 😊",
593+
"emoji_only": "πŸ”₯πŸš€βœ¨πŸ’―",
594+
"mixed": "Meeting at 3pm πŸ“… Don't forget! ⏰",
595+
}
596+
597+
response_json, response_headers = http_client._execute(
598+
method="POST",
599+
path="/messages/send",
600+
request_body=request_body,
601+
)
602+
603+
assert response_json == {"success": True}
604+
call_kwargs = patched_request.call_args[1]
605+
sent_json = call_kwargs["json"]
562606

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
607+
# All emojis should be preserved exactly
608+
assert sent_json["subject"] == "Hello πŸ‘‹ World 🌍"
609+
assert "πŸŽ‰" in sent_json["body"]
610+
assert "πŸ’ͺ" in sent_json["body"]
611+
assert "😊" in sent_json["body"]
612+
assert sent_json["emoji_only"] == "πŸ”₯πŸš€βœ¨πŸ’―"
613+
assert "πŸ“…" in sent_json["mixed"]
614+
assert "⏰" in sent_json["mixed"]
570615

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

0 commit comments

Comments
Β (0)