Skip to content

Commit cd0e3a6

Browse files
authored
fix: Set memory decompression limit (#8570)
Add size validation to compressed inputs
1 parent eb04f5f commit cd0e3a6

7 files changed

Lines changed: 489 additions & 21 deletions

File tree

qa/L0_http/http_input_size_limit_test.py

Lines changed: 191 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
sys.path.append("../common")
3131

32+
import gzip
33+
import io
3234
import json
3335
import unittest
3436

@@ -58,7 +60,7 @@ class InferSizeLimitTest(tu.TestResultCollector):
5860
def _get_infer_url(self, model_name):
5961
return "http://localhost:8000/v2/models/{}/infer".format(model_name)
6062

61-
def test_default_limit_rejection_raw_binary(self):
63+
def test_default_limit_raw_binary(self):
6264
"""Test raw binary inputs with default limit"""
6365
model = "onnx_zero_1_float32"
6466

@@ -124,7 +126,7 @@ def test_default_limit_rejection_raw_binary(self):
124126
"Response data does not match input data",
125127
)
126128

127-
def test_default_limit_rejection_json(self):
129+
def test_default_limit_json(self):
128130
"""Test JSON inputs with default limit"""
129131
model = "onnx_zero_1_float32"
130132

@@ -415,6 +417,193 @@ def test_large_string_in_json(self):
415417
error_msg,
416418
)
417419

420+
def _create_compressed_payload(self, target_size):
421+
"""Helper to create a gzip-compressed JSON payload of specified decompressed size."""
422+
shape_size = 1000 # Small actual data
423+
payload = {
424+
"inputs": [
425+
{
426+
"name": "INPUT0",
427+
"datatype": "FP32",
428+
"shape": [1, shape_size],
429+
"data": [1.0] * shape_size,
430+
}
431+
]
432+
}
433+
json_str = json.dumps(payload, indent=4)
434+
435+
# Pad with whitespace to reach target size (whitespace before closing brace is valid JSON)
436+
padding_needed = target_size - len(json_str)
437+
padded_json = json_str[:-1] + (" " * padding_needed) + json_str[-1]
438+
439+
# Compress the payload
440+
compressed_buffer = io.BytesIO()
441+
with gzip.GzipFile(fileobj=compressed_buffer, mode="wb") as gz:
442+
gz.write(padded_json.encode("utf-8"))
443+
444+
return compressed_buffer.getvalue(), len(padded_json.encode("utf-8"))
445+
446+
def test_default_limit_compressed(self):
447+
"""Test compressed inputs with default 64MB limit.
448+
449+
This test verifies that the --http-max-input-size limit is enforced on
450+
the decompressed data size, not just the compressed request size.
451+
"""
452+
model = "onnx_zero_1_float32"
453+
454+
headers = {
455+
"Content-Type": "application/json",
456+
"Content-Encoding": "gzip",
457+
}
458+
459+
# Test case 1: Payload that decompresses to 64MB + 1MB (over limit) should fail
460+
large_target_size = DEFAULT_LIMIT_BYTES + MB
461+
(
462+
large_compressed_data,
463+
large_uncompressed_size,
464+
) = self._create_compressed_payload(large_target_size)
465+
466+
# Verify uncompressed size is over 64MB limit
467+
self.assertGreater(
468+
large_uncompressed_size,
469+
DEFAULT_LIMIT_BYTES,
470+
f"Large payload should decompress to > 64MB, got {large_uncompressed_size}",
471+
)
472+
473+
# Verify compressed size is under the limit
474+
self.assertLess(
475+
len(large_compressed_data),
476+
DEFAULT_LIMIT_BYTES,
477+
f"Compressed size should be under limit, got {len(large_compressed_data)}",
478+
)
479+
480+
response = requests.post(
481+
self._get_infer_url(model), data=large_compressed_data, headers=headers
482+
)
483+
484+
# Should fail with 400 bad request - decompressed size exceeds limit
485+
self.assertEqual(
486+
400,
487+
response.status_code,
488+
f"Expected 400 for compressed request that decompresses to >64MB, got: {response.status_code}",
489+
)
490+
491+
# Verify error message contains size limit info
492+
error_msg = response.content.decode()
493+
self.assertIn(
494+
"exceeds the maximum allowed value",
495+
error_msg,
496+
"Expected error message about exceeding max input size",
497+
)
498+
499+
# Test case 2: Payload that decompresses to 64MB - 1MB (under limit) should succeed
500+
small_target_size = DEFAULT_LIMIT_BYTES - MB
501+
(
502+
small_compressed_data,
503+
small_uncompressed_size,
504+
) = self._create_compressed_payload(small_target_size)
505+
506+
# Verify uncompressed size is under 64MB limit
507+
self.assertLess(
508+
small_uncompressed_size,
509+
DEFAULT_LIMIT_BYTES,
510+
f"Small payload should decompress to < 64MB, got {small_uncompressed_size}",
511+
)
512+
513+
response = requests.post(
514+
self._get_infer_url(model), data=small_compressed_data, headers=headers
515+
)
516+
517+
# Should succeed with 200 OK
518+
self.assertEqual(
519+
200,
520+
response.status_code,
521+
f"Expected 200 for compressed request within limit, got: {response.status_code}",
522+
)
523+
524+
# Verify we got a valid response
525+
result = response.json()
526+
self.assertIn("outputs", result, "Response missing outputs field")
527+
528+
def test_large_input_compressed(self):
529+
"""Test compressed inputs with custom 128MB limit set.
530+
531+
This test verifies that compressed inputs work correctly when the
532+
--http-max-input-size limit is increased.
533+
"""
534+
model = "onnx_zero_1_float32"
535+
536+
headers = {
537+
"Content-Type": "application/json",
538+
"Content-Encoding": "gzip",
539+
}
540+
541+
# Test case 1: Input that decompresses to 128MB + 1MB (over limit) should fail
542+
large_target_size = INCREASED_LIMIT_BYTES + MB
543+
(
544+
large_compressed_data,
545+
large_uncompressed_size,
546+
) = self._create_compressed_payload(large_target_size)
547+
548+
# Verify sizes
549+
self.assertGreater(
550+
large_uncompressed_size,
551+
INCREASED_LIMIT_BYTES,
552+
f"Large payload should decompress to > 128MB, got {large_uncompressed_size}",
553+
)
554+
555+
response = requests.post(
556+
self._get_infer_url(model), data=large_compressed_data, headers=headers
557+
)
558+
559+
# Should fail with 400 bad request
560+
self.assertEqual(
561+
400,
562+
response.status_code,
563+
f"Expected 400 for compressed request exceeding 128MB limit, got: {response.status_code}",
564+
)
565+
566+
error_msg = response.content.decode()
567+
self.assertIn(
568+
"exceeds the maximum allowed value",
569+
error_msg,
570+
"Expected error message about exceeding max input size",
571+
)
572+
573+
# Test case 2: Input that decompresses to 128MB - 1MB (under limit) should succeed
574+
small_target_size = INCREASED_LIMIT_BYTES - MB
575+
(
576+
small_compressed_data,
577+
small_uncompressed_size,
578+
) = self._create_compressed_payload(small_target_size)
579+
580+
# Verify sizes
581+
self.assertLess(
582+
small_uncompressed_size,
583+
INCREASED_LIMIT_BYTES,
584+
f"Small payload should decompress to < 128MB, got {small_uncompressed_size}",
585+
)
586+
self.assertGreater(
587+
small_uncompressed_size,
588+
DEFAULT_LIMIT_BYTES,
589+
f"Small payload should decompress to > 64MB (default), got {small_uncompressed_size}",
590+
)
591+
592+
response = requests.post(
593+
self._get_infer_url(model), data=small_compressed_data, headers=headers
594+
)
595+
596+
# Should succeed with 200 OK
597+
self.assertEqual(
598+
200,
599+
response.status_code,
600+
f"Expected 200 for compressed request within 128MB limit, got: {response.status_code}",
601+
)
602+
603+
# Verify we got a valid response
604+
result = response.json()
605+
self.assertIn("outputs", result, "Response missing outputs field")
606+
418607

419608
if __name__ == "__main__":
420609
unittest.main()

qa/L0_http/test.sh

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -775,14 +775,14 @@ fi
775775

776776
set +e
777777
# Run test to verify that large inputs fail with default limit
778-
python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_rejection_raw_binary >> $CLIENT_LOG 2>&1
778+
python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_raw_binary >> $CLIENT_LOG 2>&1
779779
if [ $? -ne 0 ]; then
780780
cat $CLIENT_LOG
781781
echo -e "\n***\n*** Default Input Size Limit Test Failed for raw binary input\n***"
782782
RET=1
783783
fi
784784

785-
python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_rejection_json >> $CLIENT_LOG 2>&1
785+
python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_json >> $CLIENT_LOG 2>&1
786786
if [ $? -ne 0 ]; then
787787
cat $CLIENT_LOG
788788
echo -e "\n***\n*** Default Input Size Limit Test Failed for JSON input\n***"
@@ -795,6 +795,13 @@ if [ $? -ne 0 ]; then
795795
echo -e "\n***\n*** Default Input Size Limit Test Failed for large string in JSON\n***"
796796
RET=1
797797
fi
798+
799+
python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_compressed >> $CLIENT_LOG 2>&1
800+
if [ $? -ne 0 ]; then
801+
cat $CLIENT_LOG
802+
echo -e "\n***\n*** Default Input Size Limit Test Failed for compressed input\n***"
803+
RET=1
804+
fi
798805
set -e
799806

800807
kill $SERVER_PID
@@ -826,6 +833,13 @@ if [ $? -ne 0 ]; then
826833
echo -e "\n***\n*** Input Size Limit Test Failed for JSON input with increased limits\n***"
827834
RET=1
828835
fi
836+
837+
python http_input_size_limit_test.py InferSizeLimitTest.test_large_input_compressed >> $CLIENT_LOG 2>&1
838+
if [ $? -ne 0 ]; then
839+
cat $CLIENT_LOG
840+
echo -e "\n***\n*** Input Size Limit Test Failed for compressed input with increased limits\n***"
841+
RET=1
842+
fi
829843
set -e
830844

831845
kill $SERVER_PID

src/command_line_parser.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,8 @@ TritonParser::SetupOptions()
500500
"Number of threads handling HTTP requests."});
501501
http_options_.push_back(
502502
{OPTION_HTTP_MAX_INPUT_SIZE, "http-max-input-size", Option::ArgInt,
503-
("Maximum allowed HTTP request input size in bytes. Default is " +
503+
("Maximum allowed HTTP request input size in bytes. For compressed "
504+
"requests, this also limits the decompressed size. Default is " +
504505
std::to_string(HTTP_DEFAULT_MAX_INPUT_SIZE) + " bytes (64MB).")});
505506
http_options_.push_back(
506507
{OPTION_HTTP_RESTRICTED_API, "http-restricted-api",

0 commit comments

Comments
 (0)