diff --git a/qa/L0_http/http_input_size_limit_test.py b/qa/L0_http/http_input_size_limit_test.py index ca650e7fe3..f0ee35649b 100755 --- a/qa/L0_http/http_input_size_limit_test.py +++ b/qa/L0_http/http_input_size_limit_test.py @@ -29,6 +29,8 @@ sys.path.append("../common") +import gzip +import io import json import unittest @@ -58,7 +60,7 @@ class InferSizeLimitTest(tu.TestResultCollector): def _get_infer_url(self, model_name): return "http://localhost:8000/v2/models/{}/infer".format(model_name) - def test_default_limit_rejection_raw_binary(self): + def test_default_limit_raw_binary(self): """Test raw binary inputs with default limit""" model = "onnx_zero_1_float32" @@ -124,7 +126,7 @@ def test_default_limit_rejection_raw_binary(self): "Response data does not match input data", ) - def test_default_limit_rejection_json(self): + def test_default_limit_json(self): """Test JSON inputs with default limit""" model = "onnx_zero_1_float32" @@ -415,6 +417,193 @@ def test_large_string_in_json(self): error_msg, ) + def _create_compressed_payload(self, target_size): + """Helper to create a gzip-compressed JSON payload of specified decompressed size.""" + shape_size = 1000 # Small actual data + payload = { + "inputs": [ + { + "name": "INPUT0", + "datatype": "FP32", + "shape": [1, shape_size], + "data": [1.0] * shape_size, + } + ] + } + json_str = json.dumps(payload, indent=4) + + # Pad with whitespace to reach target size (whitespace before closing brace is valid JSON) + padding_needed = target_size - len(json_str) + padded_json = json_str[:-1] + (" " * padding_needed) + json_str[-1] + + # Compress the payload + compressed_buffer = io.BytesIO() + with gzip.GzipFile(fileobj=compressed_buffer, mode="wb") as gz: + gz.write(padded_json.encode("utf-8")) + + return compressed_buffer.getvalue(), len(padded_json.encode("utf-8")) + + def test_default_limit_compressed(self): + """Test compressed inputs with default 64MB limit. + + This test verifies that the --http-max-input-size limit is enforced on + the decompressed data size, not just the compressed request size. + """ + model = "onnx_zero_1_float32" + + headers = { + "Content-Type": "application/json", + "Content-Encoding": "gzip", + } + + # Test case 1: Payload that decompresses to 64MB + 1MB (over limit) should fail + large_target_size = DEFAULT_LIMIT_BYTES + MB + ( + large_compressed_data, + large_uncompressed_size, + ) = self._create_compressed_payload(large_target_size) + + # Verify uncompressed size is over 64MB limit + self.assertGreater( + large_uncompressed_size, + DEFAULT_LIMIT_BYTES, + f"Large payload should decompress to > 64MB, got {large_uncompressed_size}", + ) + + # Verify compressed size is under the limit + self.assertLess( + len(large_compressed_data), + DEFAULT_LIMIT_BYTES, + f"Compressed size should be under limit, got {len(large_compressed_data)}", + ) + + response = requests.post( + self._get_infer_url(model), data=large_compressed_data, headers=headers + ) + + # Should fail with 400 bad request - decompressed size exceeds limit + self.assertEqual( + 400, + response.status_code, + f"Expected 400 for compressed request that decompresses to >64MB, got: {response.status_code}", + ) + + # Verify error message contains size limit info + error_msg = response.content.decode() + self.assertIn( + "exceeds the maximum allowed value", + error_msg, + "Expected error message about exceeding max input size", + ) + + # Test case 2: Payload that decompresses to 64MB - 1MB (under limit) should succeed + small_target_size = DEFAULT_LIMIT_BYTES - MB + ( + small_compressed_data, + small_uncompressed_size, + ) = self._create_compressed_payload(small_target_size) + + # Verify uncompressed size is under 64MB limit + self.assertLess( + small_uncompressed_size, + DEFAULT_LIMIT_BYTES, + f"Small payload should decompress to < 64MB, got {small_uncompressed_size}", + ) + + response = requests.post( + self._get_infer_url(model), data=small_compressed_data, headers=headers + ) + + # Should succeed with 200 OK + self.assertEqual( + 200, + response.status_code, + f"Expected 200 for compressed request within limit, got: {response.status_code}", + ) + + # Verify we got a valid response + result = response.json() + self.assertIn("outputs", result, "Response missing outputs field") + + def test_large_input_compressed(self): + """Test compressed inputs with custom 128MB limit set. + + This test verifies that compressed inputs work correctly when the + --http-max-input-size limit is increased. + """ + model = "onnx_zero_1_float32" + + headers = { + "Content-Type": "application/json", + "Content-Encoding": "gzip", + } + + # Test case 1: Input that decompresses to 128MB + 1MB (over limit) should fail + large_target_size = INCREASED_LIMIT_BYTES + MB + ( + large_compressed_data, + large_uncompressed_size, + ) = self._create_compressed_payload(large_target_size) + + # Verify sizes + self.assertGreater( + large_uncompressed_size, + INCREASED_LIMIT_BYTES, + f"Large payload should decompress to > 128MB, got {large_uncompressed_size}", + ) + + response = requests.post( + self._get_infer_url(model), data=large_compressed_data, headers=headers + ) + + # Should fail with 400 bad request + self.assertEqual( + 400, + response.status_code, + f"Expected 400 for compressed request exceeding 128MB limit, got: {response.status_code}", + ) + + error_msg = response.content.decode() + self.assertIn( + "exceeds the maximum allowed value", + error_msg, + "Expected error message about exceeding max input size", + ) + + # Test case 2: Input that decompresses to 128MB - 1MB (under limit) should succeed + small_target_size = INCREASED_LIMIT_BYTES - MB + ( + small_compressed_data, + small_uncompressed_size, + ) = self._create_compressed_payload(small_target_size) + + # Verify sizes + self.assertLess( + small_uncompressed_size, + INCREASED_LIMIT_BYTES, + f"Small payload should decompress to < 128MB, got {small_uncompressed_size}", + ) + self.assertGreater( + small_uncompressed_size, + DEFAULT_LIMIT_BYTES, + f"Small payload should decompress to > 64MB (default), got {small_uncompressed_size}", + ) + + response = requests.post( + self._get_infer_url(model), data=small_compressed_data, headers=headers + ) + + # Should succeed with 200 OK + self.assertEqual( + 200, + response.status_code, + f"Expected 200 for compressed request within 128MB limit, got: {response.status_code}", + ) + + # Verify we got a valid response + result = response.json() + self.assertIn("outputs", result, "Response missing outputs field") + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index f34e648e3c..f5b4548c24 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -775,14 +775,14 @@ fi set +e # Run test to verify that large inputs fail with default limit -python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_rejection_raw_binary >> $CLIENT_LOG 2>&1 +python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_raw_binary >> $CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then cat $CLIENT_LOG echo -e "\n***\n*** Default Input Size Limit Test Failed for raw binary input\n***" RET=1 fi -python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_rejection_json >> $CLIENT_LOG 2>&1 +python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_json >> $CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then cat $CLIENT_LOG echo -e "\n***\n*** Default Input Size Limit Test Failed for JSON input\n***" @@ -795,6 +795,13 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Default Input Size Limit Test Failed for large string in JSON\n***" RET=1 fi + +python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_compressed >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Default Input Size Limit Test Failed for compressed input\n***" + RET=1 +fi set -e kill $SERVER_PID @@ -826,6 +833,13 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Input Size Limit Test Failed for JSON input with increased limits\n***" RET=1 fi + +python http_input_size_limit_test.py InferSizeLimitTest.test_large_input_compressed >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Input Size Limit Test Failed for compressed input with increased limits\n***" + RET=1 +fi set -e kill $SERVER_PID diff --git a/src/command_line_parser.cc b/src/command_line_parser.cc index 0d7892efcb..cc11803ae8 100644 --- a/src/command_line_parser.cc +++ b/src/command_line_parser.cc @@ -500,7 +500,8 @@ TritonParser::SetupOptions() "Number of threads handling HTTP requests."}); http_options_.push_back( {OPTION_HTTP_MAX_INPUT_SIZE, "http-max-input-size", Option::ArgInt, - ("Maximum allowed HTTP request input size in bytes. Default is " + + ("Maximum allowed HTTP request input size in bytes. For compressed " + "requests, this also limits the decompressed size. Default is " + std::to_string(HTTP_DEFAULT_MAX_INPUT_SIZE) + " bytes (64MB).")}); http_options_.push_back( {OPTION_HTTP_RESTRICTED_API, "http-restricted-api", diff --git a/src/data_compressor.h b/src/data_compressor.h index c1345e1773..a97c822816 100644 --- a/src/data_compressor.h +++ b/src/data_compressor.h @@ -112,7 +112,7 @@ class DataCompressor { RETURN_MSG_IF_ERR( AllocEVBuffer( expected_compressed_size, compressed_data, ¤t_reserved_space), - "unexpected error allocating output buffer for compression: "); + "unexpected error allocating output buffer for compression"); stream.next_out = reinterpret_cast(current_reserved_space.iov_base); stream.avail_out = expected_compressed_size; @@ -131,12 +131,12 @@ class DataCompressor { CommitEVBuffer( compressed_data, ¤t_reserved_space, expected_compressed_size), - "unexpected error committing output buffer for compression: "); + "unexpected error committing output buffer for compression"); RETURN_MSG_IF_ERR( AllocEVBuffer( expected_compressed_size, compressed_data, ¤t_reserved_space), - "unexpected error allocating output buffer for compression: "); + "unexpected error allocating output buffer for compression"); stream.next_out = reinterpret_cast(current_reserved_space.iov_base); stream.avail_out = expected_compressed_size; @@ -156,13 +156,14 @@ class DataCompressor { CommitEVBuffer( compressed_data, ¤t_reserved_space, expected_compressed_size - stream.avail_out), - "unexpected error committing output buffer for compression: "); + "unexpected error committing output buffer for compression"); } return nullptr; // success } static TRITONSERVER_Error* DecompressData( - const Type type, evbuffer* source, evbuffer* decompressed_data) + const Type type, evbuffer* source, evbuffer* decompressed_data, + const size_t max_decompressed_size = 0) { size_t source_byte_size = evbuffer_get_length(source); // nothing to be decompressed @@ -175,6 +176,13 @@ class DataCompressor { ? source_byte_size : (1 << 20 /* 1MB */); + // Cap the initial buffer allocation to the decompression limit. + // This avoids over-allocating when a decompression limit is set. + if (max_decompressed_size > 0 && + output_buffer_size > max_decompressed_size) { + output_buffer_size = max_decompressed_size; + } + switch (type) { case Type::UNKNOWN: case Type::IDENTITY: { @@ -222,12 +230,17 @@ class DataCompressor { AllocEVBuffer( output_buffer_size, decompressed_data, ¤t_reserved_space), - "unexpected error allocating output buffer for decompression: "); + "unexpected error allocating output buffer for decompression"); stream.next_out = reinterpret_cast(current_reserved_space.iov_base); stream.avail_out = output_buffer_size; - // Compress until end of 'source' + // Track total decompressed size and current buffer size for limit + // checking + size_t total_decompressed = 0; + size_t current_buffer_size = output_buffer_size; + + // Decompress until end of 'source' for (int idx = 0; idx < buffer_count; ++idx) { stream.next_in = reinterpret_cast(buffer_array[idx].iov_base); @@ -237,21 +250,59 @@ class DataCompressor { do { // Need additional buffer if (stream.avail_out == 0) { + total_decompressed += current_buffer_size; + + // Check decompression size limit before allocating memory + if (max_decompressed_size > 0 && + total_decompressed > max_decompressed_size) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Decompressed data size exceeds the maximum allowed " + "value of " + + std::to_string(max_decompressed_size) + + " bytes. Use --http-max-input-size to increase the " + "limit.") + .c_str()); + } + RETURN_MSG_IF_ERR( CommitEVBuffer( decompressed_data, ¤t_reserved_space, - output_buffer_size), + current_buffer_size), "unexpected error committing output buffer for " - "decompression: "); + "decompression"); + + // Calculate next buffer size, capped by remaining limit + current_buffer_size = output_buffer_size; + if (max_decompressed_size > 0) { + size_t remaining_size = + max_decompressed_size - total_decompressed; + // If no space remains but decompression needs more, we've hit + // the limit + if (remaining_size == 0) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Decompressed data size exceeds the maximum allowed " + "value of " + + std::to_string(max_decompressed_size) + + " bytes. Use --http-max-input-size to increase the " + "limit.") + .c_str()); + } + if (current_buffer_size > remaining_size) { + current_buffer_size = remaining_size; + } + } + RETURN_MSG_IF_ERR( AllocEVBuffer( - output_buffer_size, decompressed_data, + current_buffer_size, decompressed_data, ¤t_reserved_space), "unexpected error allocating output buffer for " - "decompression: "); + "decompression"); stream.next_out = reinterpret_cast( current_reserved_space.iov_base); - stream.avail_out = output_buffer_size; + stream.avail_out = current_buffer_size; } auto ret = inflate(&stream, Z_NO_FLUSH); if (ret == Z_STREAM_ERROR) { @@ -260,15 +311,34 @@ class DataCompressor { "encountered inconsistent stream state during " "decompression"); } + // Break if decompression is complete, even if buffer is exactly + // full + if (ret == Z_STREAM_END) { + break; + } } while (stream.avail_out == 0); } // Make sure the last buffer is committed if (current_reserved_space.iov_base != nullptr) { + size_t final_chunk_size = current_buffer_size - stream.avail_out; + if (max_decompressed_size > 0 && + (total_decompressed + final_chunk_size) > + max_decompressed_size) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Decompressed data size exceeds the maximum allowed value " + "of " + + std::to_string(max_decompressed_size) + + " bytes. Use --http-max-input-size to increase the limit.") + .c_str()); + } + RETURN_MSG_IF_ERR( CommitEVBuffer( decompressed_data, ¤t_reserved_space, - output_buffer_size - stream.avail_out), - "unexpected error committing output buffer for compression: "); + final_chunk_size), + "unexpected error committing output buffer for " + "decompression"); } break; } diff --git a/src/http_server.cc b/src/http_server.cc index bfb8150ea0..ce6bb1fe08 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -3226,7 +3226,8 @@ HTTPAPIServer::DecompressBuffer( case DataCompressor::Type::GZIP: { *decompressed_buffer = evbuffer_new(); RETURN_IF_ERR(DataCompressor::DecompressData( - compression_type, req->buffer_in, *decompressed_buffer)); + compression_type, req->buffer_in, *decompressed_buffer, + max_input_size_)); break; } case DataCompressor::Type::UNKNOWN: { diff --git a/src/sagemaker_server.cc b/src/sagemaker_server.cc index 8922ff655e..f2cf63d276 100644 --- a/src/sagemaker_server.cc +++ b/src/sagemaker_server.cc @@ -439,7 +439,8 @@ SagemakerAPIServer::SageMakerMMEHandleInfer( case DataCompressor::Type::GZIP: { decompressed_buffer = evbuffer_new(); err = DataCompressor::DecompressData( - compression_type, req->buffer_in, decompressed_buffer); + compression_type, req->buffer_in, decompressed_buffer, + max_input_size_); break; } case DataCompressor::Type::UNKNOWN: { diff --git a/src/test/data_compressor_test.cc b/src/test/data_compressor_test.cc index 2e68fd8c02..455bcd3468 100644 --- a/src/test/data_compressor_test.cc +++ b/src/test/data_compressor_test.cc @@ -51,6 +51,17 @@ namespace ni = triton::server; namespace { +// Size Constants +constexpr size_t KB = 1024; // 1 KB = 1,024 bytes +constexpr size_t MB = 1024 * KB; // 1 MB = 1,048,576 bytes + +// Triton's default HTTP max input size +constexpr size_t DEFAULT_MAX_INPUT_SIZE = 64 * MB; // 64 MB + +// Test data sizes relative to the limit +constexpr size_t UNDER_LIMIT_DATA_SIZE = DEFAULT_MAX_INPUT_SIZE - MB; // 63 MB +constexpr size_t OVER_LIMIT_DATA_SIZE = DEFAULT_MAX_INPUT_SIZE + MB; // 65 MB + struct TritonServerError { TritonServerError(TRITONSERVER_Error_Code code, const char* msg) : code_(code), msg_(msg) @@ -620,6 +631,187 @@ TEST_F(DataCompressorTest, CompressGzipBuffer) WriteEVBufferToFile("generated_gzip_compressed_data", compressed); } +// Helper to compress data with GZIP and return the compressed evbuffer +evbuffer* +CompressWithGzip(const char* data, size_t size) +{ + auto source = evbuffer_new(); + evbuffer_add(source, data, size); + auto compressed = evbuffer_new(); + ni::DataCompressor::CompressData( + ni::DataCompressor::Type::GZIP, source, compressed); + evbuffer_free(source); + return compressed; +} + +// Helper to compress data with DEFLATE and return the compressed evbuffer +evbuffer* +CompressWithDeflate(const char* data, size_t size) +{ + auto source = evbuffer_new(); + evbuffer_add(source, data, size); + auto compressed = evbuffer_new(); + ni::DataCompressor::CompressData( + ni::DataCompressor::Type::DEFLATE, source, compressed); + evbuffer_free(source); + return compressed; +} + +// This test verifies that the max_decompressed_size parameter correctly +// limits the memory allocation during GZIP decompression. +TEST_F(DataCompressorTest, DecompressionSizeLimitGzip) +{ + // Create test data buffers of different sizes + std::unique_ptr under_data(new char[UNDER_LIMIT_DATA_SIZE]); + std::unique_ptr at_data(new char[DEFAULT_MAX_INPUT_SIZE]); + std::unique_ptr over_data(new char[OVER_LIMIT_DATA_SIZE]); + memset(under_data.get(), 'A', UNDER_LIMIT_DATA_SIZE); + memset(at_data.get(), 'B', DEFAULT_MAX_INPUT_SIZE); + memset(over_data.get(), 'C', OVER_LIMIT_DATA_SIZE); + + // Compress each data set + auto under_compressed = + CompressWithGzip(under_data.get(), UNDER_LIMIT_DATA_SIZE); + auto at_compressed = CompressWithGzip(at_data.get(), DEFAULT_MAX_INPUT_SIZE); + auto over_compressed = + CompressWithGzip(over_data.get(), OVER_LIMIT_DATA_SIZE); + + // Test 1: 63 MB data with 64 MB limit - should succeed + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::GZIP, under_compressed, decompressed, + DEFAULT_MAX_INPUT_SIZE); + ASSERT_TRUE((err == nullptr)) + << "63 MB data should decompress within 64 MB limit: " + << TRITONSERVER_ErrorMessage(err); + ASSERT_EQ(evbuffer_get_length(decompressed), UNDER_LIMIT_DATA_SIZE); + evbuffer_free(decompressed); + } + + // Test 2: 64 MB data with 64 MB limit - should succeed (exact boundary) + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::GZIP, at_compressed, decompressed, + DEFAULT_MAX_INPUT_SIZE); + ASSERT_TRUE((err == nullptr)) + << "64 MB data should decompress at exact 64 MB limit: " + << TRITONSERVER_ErrorMessage(err); + ASSERT_EQ(evbuffer_get_length(decompressed), DEFAULT_MAX_INPUT_SIZE); + evbuffer_free(decompressed); + } + + // Test 3: 65 MB data with 64 MB limit - should fail + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::GZIP, over_compressed, decompressed, + DEFAULT_MAX_INPUT_SIZE); + ASSERT_TRUE((err != nullptr)) << "65 MB data should fail with 64 MB limit"; + ASSERT_EQ(TRITONSERVER_ErrorCode(err), TRITONSERVER_ERROR_INVALID_ARG); + std::string error_msg = TRITONSERVER_ErrorMessage(err); + ASSERT_TRUE( + error_msg.find("exceeds the maximum allowed") != std::string::npos) + << "Error message should mention size limit: " << error_msg; + evbuffer_free(decompressed); + } + + // Test 4: 65 MB data with no limit - should succeed + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::GZIP, over_compressed, decompressed, 0); + ASSERT_TRUE((err == nullptr)) + << "65 MB data should decompress with no limit: " + << TRITONSERVER_ErrorMessage(err); + ASSERT_EQ(evbuffer_get_length(decompressed), OVER_LIMIT_DATA_SIZE); + evbuffer_free(decompressed); + } + + evbuffer_free(under_compressed); + evbuffer_free(at_compressed); + evbuffer_free(over_compressed); +} + +// This test verifies that the max_decompressed_size parameter correctly +// limits the memory allocation during DEFLATE decompression. +TEST_F(DataCompressorTest, DecompressionSizeLimitDeflate) +{ + // Create test data buffers of different sizes + std::unique_ptr under_data(new char[UNDER_LIMIT_DATA_SIZE]); + std::unique_ptr at_data(new char[DEFAULT_MAX_INPUT_SIZE]); + std::unique_ptr over_data(new char[OVER_LIMIT_DATA_SIZE]); + memset(under_data.get(), 'A', UNDER_LIMIT_DATA_SIZE); + memset(at_data.get(), 'B', DEFAULT_MAX_INPUT_SIZE); + memset(over_data.get(), 'C', OVER_LIMIT_DATA_SIZE); + + // Compress each data set + auto under_compressed = + CompressWithDeflate(under_data.get(), UNDER_LIMIT_DATA_SIZE); + auto at_compressed = + CompressWithDeflate(at_data.get(), DEFAULT_MAX_INPUT_SIZE); + auto over_compressed = + CompressWithDeflate(over_data.get(), OVER_LIMIT_DATA_SIZE); + + // Test 1: 63 MB data with 64 MB limit - should succeed + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::DEFLATE, under_compressed, decompressed, + DEFAULT_MAX_INPUT_SIZE); + ASSERT_TRUE((err == nullptr)) + << "63 MB data should decompress within 64 MB limit: " + << TRITONSERVER_ErrorMessage(err); + ASSERT_EQ(evbuffer_get_length(decompressed), UNDER_LIMIT_DATA_SIZE); + evbuffer_free(decompressed); + } + + // Test 2: 64 MB data with 64 MB limit - should succeed (exact boundary) + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::DEFLATE, at_compressed, decompressed, + DEFAULT_MAX_INPUT_SIZE); + ASSERT_TRUE((err == nullptr)) + << "64 MB data should decompress at exact 64 MB limit: " + << TRITONSERVER_ErrorMessage(err); + ASSERT_EQ(evbuffer_get_length(decompressed), DEFAULT_MAX_INPUT_SIZE); + evbuffer_free(decompressed); + } + + // Test 3: 65 MB data with 64 MB limit - should fail + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::DEFLATE, over_compressed, decompressed, + DEFAULT_MAX_INPUT_SIZE); + ASSERT_TRUE((err != nullptr)) << "65 MB data should fail with 64 MB limit"; + ASSERT_EQ(TRITONSERVER_ErrorCode(err), TRITONSERVER_ERROR_INVALID_ARG); + std::string error_msg = TRITONSERVER_ErrorMessage(err); + ASSERT_TRUE( + error_msg.find("exceeds the maximum allowed") != std::string::npos) + << "Error message should mention size limit: " << error_msg; + evbuffer_free(decompressed); + } + + // Test 4: 65 MB data with no limit - should succeed + { + auto decompressed = evbuffer_new(); + auto err = ni::DataCompressor::DecompressData( + ni::DataCompressor::Type::DEFLATE, over_compressed, decompressed, 0); + ASSERT_TRUE((err == nullptr)) + << "65 MB data should decompress with no limit: " + << TRITONSERVER_ErrorMessage(err); + ASSERT_EQ(evbuffer_get_length(decompressed), OVER_LIMIT_DATA_SIZE); + evbuffer_free(decompressed); + } + + evbuffer_free(under_compressed); + evbuffer_free(at_compressed); + evbuffer_free(over_compressed); +} + } // namespace int