From 3ee97890cce5cc28310348fc3764ca2921f83a38 Mon Sep 17 00:00:00 2001 From: pengfeiye Date: Thu, 15 Jan 2026 18:18:14 -0500 Subject: [PATCH 1/6] rebase --- nylas/handler/http_client.py | 5 +- tests/handler/test_http_client.py | 85 ++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index ec95a21..fdb8ecb 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -91,10 +91,11 @@ def _execute( timeout = overrides["timeout"] # Serialize request_body to JSON with ensure_ascii=False to preserve UTF-8 characters - # This ensures special characters (accented letters, emoji, etc.) are not escaped + # (special characters, emoji, accented letters, etc.) and encode as UTF-8 bytes + # to avoid Latin-1 encoding errors in the HTTP layer json_data = None if request_body is not None and data is None: - json_data = json.dumps(request_body, ensure_ascii=False) + json_data = json.dumps(request_body, ensure_ascii=False).encode('utf-8') try: response = requests.request( request["method"], diff --git a/tests/handler/test_http_client.py b/tests/handler/test_http_client.py index 76767f7..af25b69 100644 --- a/tests/handler/test_http_client.py +++ b/tests/handler/test_http_client.py @@ -302,7 +302,7 @@ def test_execute(self, http_client, patched_version_and_sys, patched_request): "Content-type": "application/json; charset=utf-8", "test": "header", }, - data='{"foo": "bar"}', + data=b'{"foo": "bar"}', timeout=30, ) @@ -336,7 +336,7 @@ def test_execute_override_timeout( "Content-type": "application/json; charset=utf-8", "test": "header", }, - data='{"foo": "bar"}', + data=b'{"foo": "bar"}', timeout=60, ) @@ -426,12 +426,12 @@ def test_execute_with_headers(self, http_client, patched_version_and_sys, patche "Content-type": "application/json; charset=utf-8", "test": "header", }, - data='{"foo": "bar"}', + data=b'{"foo": "bar"}', timeout=30, ) def test_execute_with_utf8_characters(self, http_client, patched_version_and_sys, patched_request): - """Test that UTF-8 characters are preserved in JSON requests (not escaped).""" + """Test that UTF-8 characters are safely encoded in JSON requests.""" mock_response = Mock() mock_response.json.return_value = {"success": True} mock_response.headers = {"X-Test-Header": "test"} @@ -452,17 +452,21 @@ def test_execute_with_utf8_characters(self, http_client, patched_version_and_sys ) assert response_json == {"success": True} - # Verify that the data sent preserves UTF-8 characters (not escaped) + # Verify that the data is sent as UTF-8 encoded bytes call_kwargs = patched_request.call_args[1] assert "data" in call_kwargs sent_data = call_kwargs["data"] - # The JSON should contain actual UTF-8 characters, not escape sequences - assert "Réunion d'équipe" in sent_data - assert "De l'idée à la post-prod" in sent_data - assert "café" in sent_data + # Data should be bytes + assert isinstance(sent_data, bytes) + + # The JSON should contain actual UTF-8 characters (not escaped) + decoded = sent_data.decode('utf-8') + assert "Réunion d'équipe" in decoded + assert "De l'idée à la post-prod" in decoded + assert "café" in decoded # Should NOT contain unicode escape sequences - assert "\\u" not in sent_data + assert "\\u" not in decoded def test_execute_with_none_request_body(self, http_client, patched_version_and_sys, patched_request): """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 assert call_kwargs["data"] is None def test_execute_with_emoji_and_international_characters(self, http_client, patched_version_and_sys, patched_request): - """Test that emoji and various international characters are preserved.""" + """Test that emoji and various international characters are safely encoded.""" mock_response = Mock() mock_response.json.return_value = {"success": True} mock_response.headers = {"X-Test-Header": "test"} @@ -511,13 +515,58 @@ def test_execute_with_emoji_and_international_characters(self, http_client, patc call_kwargs = patched_request.call_args[1] sent_data = call_kwargs["data"] - # All characters should be preserved - assert "🎉 Party time! 🥳" in sent_data - assert "こんにちは" in sent_data - assert "你好" in sent_data - assert "Привет" in sent_data - assert "Größe" in sent_data - assert "¿Cómo estás?" in sent_data + # Data should be bytes + assert isinstance(sent_data, bytes) + + # All characters should be preserved (not escaped) + decoded = sent_data.decode('utf-8') + assert "🎉 Party time! 🥳" in decoded + assert "こんにちは" in decoded + assert "你好" in decoded + assert "Привет" in decoded + assert "Größe" in decoded + assert "¿Cómo estás?" in decoded + # Should NOT contain unicode escape sequences + assert "\\u" not in decoded + + def test_execute_with_right_single_quotation_mark(self, http_client, patched_version_and_sys, patched_request): + """Test that right single quotation mark (\\u2019) is handled correctly. + + This character caused UnicodeEncodeError: 'latin-1' codec can't encode character '\\u2019'. + """ + mock_response = Mock() + mock_response.json.return_value = {"success": True} + mock_response.headers = {"X-Test-Header": "test"} + mock_response.status_code = 200 + patched_request.return_value = mock_response + + # The \u2019 character is the right single quotation mark (') + # This was the exact character that caused the original encoding error + request_body = { + "subject": "It's a test", # Contains \u2019 (right single quotation mark) + "body": "Here's another example with curly apostrophe", + } + + response_json, response_headers = http_client._execute( + method="POST", + path="/messages/send", + request_body=request_body, + ) + + assert response_json == {"success": True} + call_kwargs = patched_request.call_args[1] + sent_data = call_kwargs["data"] + + # Data should be bytes + assert isinstance(sent_data, bytes) + + # The character should be preserved (not escaped) + decoded = sent_data.decode('utf-8') + assert "'" in decoded # \u2019 right single quotation mark + assert "It's a test" in decoded + assert "Here's another" in decoded + # Should NOT contain unicode escape sequences + assert "\\u2019" not in decoded def test_execute_with_multipart_data_not_affected(self, http_client, patched_version_and_sys, patched_request): """Test that multipart/form-data is not affected by the change.""" From f3178c2b81acda323d81623cdadc65e131646dcc Mon Sep 17 00:00:00 2001 From: pengfeiye Date: Thu, 15 Jan 2026 18:24:56 -0500 Subject: [PATCH 2/6] more update --- nylas/handler/http_client.py | 31 +++++--- tests/handler/test_http_client.py | 127 ++++++++++++++++++++---------- 2 files changed, 104 insertions(+), 54 deletions(-) diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index fdb8ecb..4ade7c2 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -90,20 +90,25 @@ def _execute( if overrides and overrides.get("timeout"): timeout = overrides["timeout"] - # Serialize request_body to JSON with ensure_ascii=False to preserve UTF-8 characters - # (special characters, emoji, accented letters, etc.) and encode as UTF-8 bytes - # to avoid Latin-1 encoding errors in the HTTP layer - json_data = None - if request_body is not None and data is None: - json_data = json.dumps(request_body, ensure_ascii=False).encode('utf-8') + # Use requests' json parameter for proper UTF-8 handling when sending JSON + # This avoids Latin-1 encoding errors with special characters (emoji, accented letters, etc.) try: - response = requests.request( - request["method"], - request["url"], - headers=request["headers"], - data=json_data or data, - timeout=timeout, - ) + if request_body is not None and data is None: + response = requests.request( + request["method"], + request["url"], + headers=request["headers"], + json=request_body, + timeout=timeout, + ) + else: + response = requests.request( + request["method"], + request["url"], + headers=request["headers"], + data=data, + timeout=timeout, + ) except requests.exceptions.Timeout as exc: raise NylasSdkTimeoutError(url=request["url"], timeout=timeout) from exc diff --git a/tests/handler/test_http_client.py b/tests/handler/test_http_client.py index af25b69..ca9d7d4 100644 --- a/tests/handler/test_http_client.py +++ b/tests/handler/test_http_client.py @@ -302,7 +302,7 @@ def test_execute(self, http_client, patched_version_and_sys, patched_request): "Content-type": "application/json; charset=utf-8", "test": "header", }, - data=b'{"foo": "bar"}', + json={"foo": "bar"}, timeout=30, ) @@ -336,7 +336,7 @@ def test_execute_override_timeout( "Content-type": "application/json; charset=utf-8", "test": "header", }, - data=b'{"foo": "bar"}', + json={"foo": "bar"}, timeout=60, ) @@ -426,7 +426,7 @@ def test_execute_with_headers(self, http_client, patched_version_and_sys, patche "Content-type": "application/json; charset=utf-8", "test": "header", }, - data=b'{"foo": "bar"}', + json={"foo": "bar"}, timeout=30, ) @@ -452,21 +452,15 @@ def test_execute_with_utf8_characters(self, http_client, patched_version_and_sys ) assert response_json == {"success": True} - # Verify that the data is sent as UTF-8 encoded bytes + # Verify that the json parameter is used with original data call_kwargs = patched_request.call_args[1] - assert "data" in call_kwargs - sent_data = call_kwargs["data"] - - # Data should be bytes - assert isinstance(sent_data, bytes) + assert "json" in call_kwargs + sent_json = call_kwargs["json"] - # The JSON should contain actual UTF-8 characters (not escaped) - decoded = sent_data.decode('utf-8') - assert "Réunion d'équipe" in decoded - assert "De l'idée à la post-prod" in decoded - assert "café" in decoded - # Should NOT contain unicode escape sequences - assert "\\u" not in decoded + # The JSON should contain actual UTF-8 characters + assert sent_json["title"] == "Réunion d'équipe" + assert sent_json["description"] == "De l'idée à la post-prod, sans friction" + assert sent_json["location"] == "café" def test_execute_with_none_request_body(self, http_client, patched_version_and_sys, patched_request): """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 ) assert response_json == {"success": True} - # Verify that data is None when request_body is None + # Verify that data branch is used when request_body is None call_kwargs = patched_request.call_args[1] + # Should use data= parameter, not json= parameter assert "data" in call_kwargs + assert "json" not in call_kwargs + assert call_kwargs["data"] is None + + def test_execute_with_none_request_body_and_none_data(self, http_client, patched_version_and_sys, patched_request): + """Test that both None request_body and None data are handled correctly.""" + mock_response = Mock() + mock_response.json.return_value = {"success": True} + mock_response.headers = {"X-Test-Header": "test"} + mock_response.status_code = 200 + patched_request.return_value = mock_response + + response_json, response_headers = http_client._execute( + method="DELETE", + path="/events/123", + request_body=None, + data=None, + ) + + assert response_json == {"success": True} + call_kwargs = patched_request.call_args[1] + # Should use data= parameter with None value + assert "data" in call_kwargs + assert "json" not in call_kwargs assert call_kwargs["data"] is None 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 assert response_json == {"success": True} call_kwargs = patched_request.call_args[1] - sent_data = call_kwargs["data"] + sent_json = call_kwargs["json"] - # Data should be bytes - assert isinstance(sent_data, bytes) - - # All characters should be preserved (not escaped) - decoded = sent_data.decode('utf-8') - assert "🎉 Party time! 🥳" in decoded - assert "こんにちは" in decoded - assert "你好" in decoded - assert "Привет" in decoded - assert "Größe" in decoded - assert "¿Cómo estás?" in decoded - # Should NOT contain unicode escape sequences - assert "\\u" not in decoded + # All characters should be preserved in the json dict + assert sent_json["emoji"] == "🎉 Party time! 🥳" + assert sent_json["japanese"] == "こんにちは" + assert sent_json["chinese"] == "你好" + assert sent_json["russian"] == "Привет" + assert sent_json["german"] == "Größe" + assert sent_json["spanish"] == "¿Cómo estás?" def test_execute_with_right_single_quotation_mark(self, http_client, patched_version_and_sys, patched_request): """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 assert response_json == {"success": True} call_kwargs = patched_request.call_args[1] - sent_data = call_kwargs["data"] + sent_json = call_kwargs["json"] + + # The \u2019 character should be preserved + assert "'" in sent_json["subject"] # \u2019 right single quotation mark + assert sent_json["subject"] == "It's a test" + assert "'" in sent_json["body"] + assert "Here's another" in sent_json["body"] + + def test_execute_with_emojis(self, http_client, patched_version_and_sys, patched_request): + """Test that emojis are handled correctly in request bodies. - # Data should be bytes - assert isinstance(sent_data, bytes) + Emojis are multi-byte UTF-8 characters that could cause encoding issues + if not handled properly. + """ + mock_response = Mock() + mock_response.json.return_value = {"success": True} + mock_response.headers = {"X-Test-Header": "test"} + mock_response.status_code = 200 + patched_request.return_value = mock_response + + request_body = { + "subject": "Hello 👋 World 🌍", + "body": "Great job! 🎉 Keep up the good work 💪 See you soon 😊", + "emoji_only": "🔥🚀✨💯", + "mixed": "Meeting at 3pm 📅 Don't forget! ⏰", + } + + response_json, response_headers = http_client._execute( + method="POST", + path="/messages/send", + request_body=request_body, + ) + + assert response_json == {"success": True} + call_kwargs = patched_request.call_args[1] + sent_json = call_kwargs["json"] - # The character should be preserved (not escaped) - decoded = sent_data.decode('utf-8') - assert "'" in decoded # \u2019 right single quotation mark - assert "It's a test" in decoded - assert "Here's another" in decoded - # Should NOT contain unicode escape sequences - assert "\\u2019" not in decoded + # All emojis should be preserved exactly + assert sent_json["subject"] == "Hello 👋 World 🌍" + assert "🎉" in sent_json["body"] + assert "💪" in sent_json["body"] + assert "😊" in sent_json["body"] + assert sent_json["emoji_only"] == "🔥🚀✨💯" + assert "📅" in sent_json["mixed"] + assert "⏰" in sent_json["mixed"] def test_execute_with_multipart_data_not_affected(self, http_client, patched_version_and_sys, patched_request): """Test that multipart/form-data is not affected by the change.""" From 7e6421ccc6163e6fcdd8eaf7e25c698b06c09df2 Mon Sep 17 00:00:00 2001 From: pengfeiye Date: Thu, 15 Jan 2026 18:27:36 -0500 Subject: [PATCH 3/6] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f382ed..4c7ab3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ nylas-python Changelog ====================== +Unrelease +---------- +* Update to use requests' json parameter for proper UTF-8 handling when sending JSON + v6.14.1 ---------- * Fix attachment id to not be a requirement From 327de1765b282c5b85ab45734249a591728c392b Mon Sep 17 00:00:00 2001 From: pengfeiye Date: Thu, 15 Jan 2026 19:05:17 -0500 Subject: [PATCH 4/6] lint --- nylas/handler/http_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index 4ade7c2..a8a7597 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -1,5 +1,4 @@ import sys -import json from typing import Union, Tuple, Dict from urllib.parse import urlparse, quote From 486c99ecbe5a0ec847c060ea30ba2a8e51a66170 Mon Sep 17 00:00:00 2001 From: pengfeiye Date: Thu, 15 Jan 2026 19:19:11 -0500 Subject: [PATCH 5/6] update --- CHANGELOG.md | 5 +- nylas/handler/http_client.py | 32 ++++---- tests/handler/test_http_client.py | 119 +++++++++++++++++++++--------- 3 files changed, 101 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7ab3e..3092a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ nylas-python Changelog ====================== -Unrelease +Unreleased ---------- -* Update to use requests' json parameter for proper UTF-8 handling when sending JSON +* Fix UTF-8 encoding for special characters (emoji, accented letters, etc.) by encoding JSON as UTF-8 bytes +* Maintain support for NaN and Infinity float values (using allow_nan=True in JSON serialization) v6.14.1 ---------- diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index a8a7597..9027736 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -1,3 +1,4 @@ +import json import sys from typing import Union, Tuple, Dict from urllib.parse import urlparse, quote @@ -89,25 +90,20 @@ def _execute( if overrides and overrides.get("timeout"): timeout = overrides["timeout"] - # Use requests' json parameter for proper UTF-8 handling when sending JSON - # This avoids Latin-1 encoding errors with special characters (emoji, accented letters, etc.) + # Serialize request_body to JSON with ensure_ascii=False to preserve UTF-8 characters + # and allow_nan=True to support NaN/Infinity values (matching default json.dumps behavior). + # Encode as UTF-8 bytes to avoid Latin-1 encoding errors with special characters. + json_data = None + if request_body is not None and data is None: + json_data = json.dumps(request_body, ensure_ascii=False, allow_nan=True).encode("utf-8") try: - if request_body is not None and data is None: - response = requests.request( - request["method"], - request["url"], - headers=request["headers"], - json=request_body, - timeout=timeout, - ) - else: - response = requests.request( - request["method"], - request["url"], - headers=request["headers"], - data=data, - timeout=timeout, - ) + response = requests.request( + request["method"], + request["url"], + headers=request["headers"], + data=json_data if json_data is not None else data, + timeout=timeout, + ) except requests.exceptions.Timeout as exc: raise NylasSdkTimeoutError(url=request["url"], timeout=timeout) from exc diff --git a/tests/handler/test_http_client.py b/tests/handler/test_http_client.py index ca9d7d4..f9511d4 100644 --- a/tests/handler/test_http_client.py +++ b/tests/handler/test_http_client.py @@ -302,7 +302,7 @@ def test_execute(self, http_client, patched_version_and_sys, patched_request): "Content-type": "application/json; charset=utf-8", "test": "header", }, - json={"foo": "bar"}, + data=b'{"foo": "bar"}', timeout=30, ) @@ -336,7 +336,7 @@ def test_execute_override_timeout( "Content-type": "application/json; charset=utf-8", "test": "header", }, - json={"foo": "bar"}, + data=b'{"foo": "bar"}', timeout=60, ) @@ -426,12 +426,12 @@ def test_execute_with_headers(self, http_client, patched_version_and_sys, patche "Content-type": "application/json; charset=utf-8", "test": "header", }, - json={"foo": "bar"}, + data=b'{"foo": "bar"}', timeout=30, ) def test_execute_with_utf8_characters(self, http_client, patched_version_and_sys, patched_request): - """Test that UTF-8 characters are safely encoded in JSON requests.""" + """Test that UTF-8 characters are preserved in JSON requests (not escaped).""" mock_response = Mock() mock_response.json.return_value = {"success": True} mock_response.headers = {"X-Test-Header": "test"} @@ -452,15 +452,19 @@ def test_execute_with_utf8_characters(self, http_client, patched_version_and_sys ) assert response_json == {"success": True} - # Verify that the json parameter is used with original data + # Verify that the data is sent as UTF-8 encoded bytes call_kwargs = patched_request.call_args[1] - assert "json" in call_kwargs - sent_json = call_kwargs["json"] + assert "data" in call_kwargs + sent_data = call_kwargs["data"] - # The JSON should contain actual UTF-8 characters - assert sent_json["title"] == "Réunion d'équipe" - assert sent_json["description"] == "De l'idée à la post-prod, sans friction" - assert sent_json["location"] == "café" + # The data should be bytes with actual UTF-8 characters (not escape sequences) + assert isinstance(sent_data, bytes) + decoded_data = sent_data.decode("utf-8") + assert "Réunion d'équipe" in decoded_data + assert "De l'idée à la post-prod, sans friction" in decoded_data + assert "café" in decoded_data + # Should NOT contain unicode escape sequences + assert "\\u" not in decoded_data def test_execute_with_none_request_body(self, http_client, patched_version_and_sys, patched_request): """Test that None request_body is handled correctly.""" @@ -507,7 +511,7 @@ def test_execute_with_none_request_body_and_none_data(self, http_client, patched assert call_kwargs["data"] is None def test_execute_with_emoji_and_international_characters(self, http_client, patched_version_and_sys, patched_request): - """Test that emoji and various international characters are safely encoded.""" + """Test that emoji and various international characters are preserved.""" mock_response = Mock() mock_response.json.return_value = {"success": True} mock_response.headers = {"X-Test-Header": "test"} @@ -531,15 +535,17 @@ def test_execute_with_emoji_and_international_characters(self, http_client, patc assert response_json == {"success": True} call_kwargs = patched_request.call_args[1] - sent_json = call_kwargs["json"] + sent_data = call_kwargs["data"] - # All characters should be preserved in the json dict - assert sent_json["emoji"] == "🎉 Party time! 🥳" - assert sent_json["japanese"] == "こんにちは" - assert sent_json["chinese"] == "你好" - assert sent_json["russian"] == "Привет" - assert sent_json["german"] == "Größe" - assert sent_json["spanish"] == "¿Cómo estás?" + # All characters should be preserved as UTF-8 encoded bytes + assert isinstance(sent_data, bytes) + decoded_data = sent_data.decode("utf-8") + assert "🎉 Party time! 🥳" in decoded_data + assert "こんにちは" in decoded_data + assert "你好" in decoded_data + assert "Привет" in decoded_data + assert "Größe" in decoded_data + assert "¿Cómo estás?" in decoded_data def test_execute_with_right_single_quotation_mark(self, http_client, patched_version_and_sys, patched_request): """Test that right single quotation mark (\\u2019) is handled correctly. @@ -567,13 +573,14 @@ def test_execute_with_right_single_quotation_mark(self, http_client, patched_ver assert response_json == {"success": True} call_kwargs = patched_request.call_args[1] - sent_json = call_kwargs["json"] + sent_data = call_kwargs["data"] - # The \u2019 character should be preserved - assert "'" in sent_json["subject"] # \u2019 right single quotation mark - assert sent_json["subject"] == "It's a test" - assert "'" in sent_json["body"] - assert "Here's another" in sent_json["body"] + # The data should be UTF-8 encoded bytes with the \u2019 character preserved + assert isinstance(sent_data, bytes) + decoded_data = sent_data.decode("utf-8") + assert "'" in decoded_data # \u2019 right single quotation mark + assert "It's a test" in decoded_data + assert "Here's another" in decoded_data def test_execute_with_emojis(self, http_client, patched_version_and_sys, patched_request): """Test that emojis are handled correctly in request bodies. @@ -602,16 +609,58 @@ def test_execute_with_emojis(self, http_client, patched_version_and_sys, patched assert response_json == {"success": True} call_kwargs = patched_request.call_args[1] - sent_json = call_kwargs["json"] + sent_data = call_kwargs["data"] + + # All emojis should be preserved in UTF-8 encoded bytes + assert isinstance(sent_data, bytes) + decoded_data = sent_data.decode("utf-8") + assert "Hello 👋 World 🌍" in decoded_data + assert "🎉" in decoded_data + assert "💪" in decoded_data + assert "😊" in decoded_data + assert "🔥🚀✨💯" in decoded_data + assert "📅" in decoded_data + assert "⏰" in decoded_data + + def test_execute_with_nan_and_infinity(self, http_client, patched_version_and_sys, patched_request): + """Test that NaN and Infinity float values are handled correctly. + + The requests library's json= parameter uses allow_nan=False which raises + ValueError for NaN/Infinity. Our implementation uses json.dumps with + allow_nan=True to maintain backward compatibility. + """ + mock_response = Mock() + mock_response.json.return_value = {"success": True} + mock_response.headers = {"X-Test-Header": "test"} + mock_response.status_code = 200 + patched_request.return_value = mock_response + + request_body = { + "nan_value": float("nan"), + "infinity": float("inf"), + "neg_infinity": float("-inf"), + "normal": 42.5, + } + + # This should NOT raise ValueError + response_json, response_headers = http_client._execute( + method="POST", + path="/data", + request_body=request_body, + ) + + assert response_json == {"success": True} + call_kwargs = patched_request.call_args[1] + sent_data = call_kwargs["data"] - # All emojis should be preserved exactly - assert sent_json["subject"] == "Hello 👋 World 🌍" - assert "🎉" in sent_json["body"] - assert "💪" in sent_json["body"] - assert "😊" in sent_json["body"] - assert sent_json["emoji_only"] == "🔥🚀✨💯" - assert "📅" in sent_json["mixed"] - assert "⏰" in sent_json["mixed"] + # The data should be UTF-8 encoded bytes with NaN/Infinity serialized + assert isinstance(sent_data, bytes) + decoded_data = sent_data.decode("utf-8") + # json.dumps with allow_nan=True produces NaN, Infinity, -Infinity (JS-style) + assert "NaN" in decoded_data + assert "Infinity" in decoded_data + assert "-Infinity" in decoded_data + assert "42.5" in decoded_data def test_execute_with_multipart_data_not_affected(self, http_client, patched_version_and_sys, patched_request): """Test that multipart/form-data is not affected by the change.""" From 103c3b662f2b03c5befecb40d08ce4ee4ba180d3 Mon Sep 17 00:00:00 2001 From: pengfeiye Date: Thu, 15 Jan 2026 19:20:40 -0500 Subject: [PATCH 6/6] update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3092a82..75d9174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ nylas-python Changelog Unreleased ---------- * Fix UTF-8 encoding for special characters (emoji, accented letters, etc.) by encoding JSON as UTF-8 bytes -* Maintain support for NaN and Infinity float values (using allow_nan=True in JSON serialization) v6.14.1 ----------