Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion runtime/python_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import uuid
from pathlib import Path, PurePath

from safe_codec import SafeCodec, CodecError

# Ensure the working directory is importable so local modules can be resolved when
# the bridge is launched as a script from a different directory.
try:
Expand Down Expand Up @@ -95,6 +97,16 @@ def get_codec_max_bytes():
# Why: parse once at startup to avoid per-response env lookups.
CODEC_MAX_BYTES = get_codec_max_bytes()

# Why: use SafeCodec for final JSON encoding to reject NaN/Infinity and handle
# edge cases like numpy scalars. We use sys.maxsize for SafeCodec's internal limit
# to preserve the original "no limit unless TYWRAP_CODEC_MAX_BYTES is set" behavior.
# The explicit size check in encode_response() provides the specific error message
# mentioning the env var name, which is important for debugging.
_response_codec = SafeCodec(
allow_nan=False,
max_payload_bytes=sys.maxsize,
)


def get_request_max_bytes():
"""
Expand Down Expand Up @@ -779,8 +791,13 @@ def encode_response(out):
Serialize the response and enforce size limits.

Why: keep payload size checks outside the main loop for clarity and lint compliance.
Uses SafeCodec to reject NaN/Infinity and handle edge cases like numpy scalars.
"""
payload = json.dumps(out)
try:
payload = _response_codec.encode(out)
except CodecError as exc:
# Convert CodecError to ValueError for consistent error handling
raise ValueError(str(exc)) from exc
payload_bytes = len(payload.encode('utf-8'))
if CODEC_MAX_BYTES is not None and payload_bytes > CODEC_MAX_BYTES:
raise PayloadTooLargeError(payload_bytes, CODEC_MAX_BYTES)
Expand Down
2 changes: 1 addition & 1 deletion test/adversarial_playground.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ describeAdversarial('Adversarial playground', () => {

try {
await expect(callAdversarial(bridge, 'return_nan_payload', [])).rejects.toThrow(
/Protocol error|Invalid JSON|JSON parse failed/
/Protocol error|Invalid JSON|JSON parse failed|Cannot serialize NaN|NaN.*not allowed/
);
} finally {
await bridge.dispose();
Expand Down
4 changes: 3 additions & 1 deletion test/fixtures/python/adversarial_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ def return_unserializable() -> Any:
"""Return a non-JSON-serializable value.

Why: ensure serialization failures surface as explicit errors.
Note: sets are now serialized as lists, so we return a function which
cannot be JSON serialized.
"""
return {1, 2, 3}
return lambda x: x


def return_circular_reference() -> list:
Expand Down
Loading