From e2170ec27dfcce17c29387c9edb92829d6e8febf Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Fri, 19 May 2023 09:22:41 -0700 Subject: [PATCH 01/21] Override _read_body to allow max size to be read. --- tabpy/tabpy_server/app/app.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index e4397d94..20a11b18 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -27,6 +27,7 @@ UploadDestinationHandler, ) import tornado +from tornado.http1connection import HTTP1Connection import tabpy.tabpy_server.app.arrow_server as pa import _thread @@ -497,3 +498,16 @@ def _build_tabpy_state(self): logger.info(f"Loading state from state file {state_file_path}") tabpy_state = _get_state_from_file(state_file_dir) return tabpy_state, TabPyState(config=tabpy_state, settings=self.settings) + + +# Override _read_body to allow content with size exceeding max_body_size +# This enables proper handling of 413 errors in base_handler +def _read_body_allow_max_size(self, code, headers, delegate): + if "Content-Length" in headers: + content_length = int(headers["Content-Length"]) + if content_length > self._max_body_size: + return + return self.original_read_body(code, headers, delegate) + +HTTP1Connection.original_read_body = HTTP1Connection._read_body +HTTP1Connection._read_body = _read_body_allow_max_size From d7ee6d7dae587eb1a8009da01100f5ced4727221 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Fri, 19 May 2023 09:23:51 -0700 Subject: [PATCH 02/21] Change max_request_size to class-level. --- tabpy/tabpy_server/app/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index 20a11b18..07626bee 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -63,6 +63,7 @@ class TabPyApp: python_service = None credentials = {} arrow_server = None + max_request_size = 0 def __init__(self, config_file): if config_file is None: @@ -117,10 +118,10 @@ def _get_arrow_server(self, config): def run(self): application = self._create_tornado_web_app() - max_request_size = ( + self.max_request_size = ( int(self.settings[SettingsParameters.MaxRequestSizeInMb]) * 1024 * 1024 ) - logger.info(f"Setting max request size to {max_request_size} bytes") + logger.info(f"Setting max request size to {self.max_request_size} bytes") init_model_evaluator(self.settings, self.tabpy_state, self.python_service) @@ -143,8 +144,8 @@ def run(self): application.listen( self.settings[SettingsParameters.Port], ssl_options=ssl_options, - max_buffer_size=max_request_size, - max_body_size=max_request_size, + max_buffer_size=self.max_request_size, + max_body_size=self.max_request_size, **settings, ) From 02089e2bb801d560e2bf13c6e13d3499b6c950f2 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Fri, 19 May 2023 09:24:12 -0700 Subject: [PATCH 03/21] Add request_body_size_within_limit method. --- tabpy/tabpy_server/handlers/base_handler.py | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tabpy/tabpy_server/handlers/base_handler.py b/tabpy/tabpy_server/handlers/base_handler.py index 3d522b03..0a418a0c 100644 --- a/tabpy/tabpy_server/handlers/base_handler.py +++ b/tabpy/tabpy_server/handlers/base_handler.py @@ -127,6 +127,7 @@ def initialize(self, app): self.username = None self.password = None self.eval_timeout = self.settings[SettingsParameters.EvaluateTimeout] + self.max_request_size = app.max_request_size self.logger = ContextLoggerWrapper(self.request) self.logger.enable_context_logging( @@ -442,3 +443,27 @@ def fail_with_auth_error(self): info="Not Acceptable", log_message="Username or password provided when authentication not available.", ) + + def request_body_size_within_limit(self): + """ + Determines if the request body size is within the specified limit. + + Returns + ------- + bool + True if the request body size is within the limit, False otherwise. + """ + headers = self.request.headers + + if "Content-Length" in headers: + content_length = int(headers["Content-Length"]) + + if content_length > self.max_request_size: + self.error_out( + 413, + info="Request Entity Too Large", + log_message="Request body size exceeds the specified limit.", + ) + return False + + return True From 561983c43243af0351fc5ae7bc4ad70a28665999 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Fri, 19 May 2023 09:24:31 -0700 Subject: [PATCH 04/21] Add request_body_size_within_limit check to eval. --- tabpy/tabpy_server/handlers/evaluation_plane_handler.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tabpy/tabpy_server/handlers/evaluation_plane_handler.py b/tabpy/tabpy_server/handlers/evaluation_plane_handler.py index 7fcdf03c..4918d9f6 100644 --- a/tabpy/tabpy_server/handlers/evaluation_plane_handler.py +++ b/tabpy/tabpy_server/handlers/evaluation_plane_handler.py @@ -46,6 +46,10 @@ def post(self): if self.should_fail_with_auth_error() != AuthErrorStates.NONE: self.fail_with_auth_error() return + + if not self.request_body_size_within_limit(): + return + self.error_out(404, "Ad-hoc scripts have been disabled on this analytics extension, please contact your " "administrator.") @@ -165,6 +169,9 @@ def post(self): if self.should_fail_with_auth_error() != AuthErrorStates.NONE: self.fail_with_auth_error() return + + if not self.request_body_size_within_limit(): + return self._add_CORS_header() try: From 25cb6555d08c8445330acd7a98a5c951f05bd491 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Fri, 19 May 2023 09:24:39 -0700 Subject: [PATCH 05/21] Add request_body_size_within_limit check to query. --- tabpy/tabpy_server/handlers/query_plane_handler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tabpy/tabpy_server/handlers/query_plane_handler.py b/tabpy/tabpy_server/handlers/query_plane_handler.py index 004af2b3..1214655d 100644 --- a/tabpy/tabpy_server/handlers/query_plane_handler.py +++ b/tabpy/tabpy_server/handlers/query_plane_handler.py @@ -217,6 +217,9 @@ def get(self, endpoint_name): self.fail_with_auth_error() return + if not self.request_body_size_within_limit(): + return + start = time.time() endpoint_name = urllib.parse.unquote(endpoint_name) self._process_query(endpoint_name, start) @@ -229,6 +232,9 @@ def post(self, endpoint_name): self.fail_with_auth_error() return + if not self.request_body_size_within_limit(): + return + start = time.time() endpoint_name = urllib.parse.unquote(endpoint_name) self._process_query(endpoint_name, start) From 5acb6126bbec1ba44792f906b27f6b8088b6bf82 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Fri, 19 May 2023 09:25:15 -0700 Subject: [PATCH 06/21] Bump version to 2.8.0. --- tabpy/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabpy/VERSION b/tabpy/VERSION index 860487ca..834f2629 100755 --- a/tabpy/VERSION +++ b/tabpy/VERSION @@ -1 +1 @@ -2.7.1 +2.8.0 From 156c7d9d7dfc1e3c625aec22f7074434fdbb688b Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 17:53:37 -0700 Subject: [PATCH 07/21] Log request size and limit on error. --- tabpy/tabpy_server/handlers/base_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabpy/tabpy_server/handlers/base_handler.py b/tabpy/tabpy_server/handlers/base_handler.py index 0a418a0c..c50560a5 100644 --- a/tabpy/tabpy_server/handlers/base_handler.py +++ b/tabpy/tabpy_server/handlers/base_handler.py @@ -462,7 +462,7 @@ def request_body_size_within_limit(self): self.error_out( 413, info="Request Entity Too Large", - log_message="Request body size exceeds the specified limit.", + log_message=f"Request with size {content_length} exceeded limit of {self.max_request_size} (bytes).", ) return False From e35346fc66ed3f6013f400ddf5593eb380bd7a33 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 18:04:03 -0700 Subject: [PATCH 08/21] Defaut max_request_size class var to None. --- tabpy/tabpy_server/app/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index 07626bee..e646f8ba 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -63,7 +63,7 @@ class TabPyApp: python_service = None credentials = {} arrow_server = None - max_request_size = 0 + max_request_size = None def __init__(self, config_file): if config_file is None: @@ -121,6 +121,7 @@ def run(self): self.max_request_size = ( int(self.settings[SettingsParameters.MaxRequestSizeInMb]) * 1024 * 1024 ) + print(f"Setting max request size to {self.max_request_size} bytes") logger.info(f"Setting max request size to {self.max_request_size} bytes") init_model_evaluator(self.settings, self.tabpy_state, self.python_service) From a2128aed9246fbd6741bc4e6fab36f39990607fe Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 18:04:15 -0700 Subject: [PATCH 09/21] Check if max_request_size is set. --- tabpy/tabpy_server/handlers/base_handler.py | 24 ++++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tabpy/tabpy_server/handlers/base_handler.py b/tabpy/tabpy_server/handlers/base_handler.py index c50560a5..3b5ae0cc 100644 --- a/tabpy/tabpy_server/handlers/base_handler.py +++ b/tabpy/tabpy_server/handlers/base_handler.py @@ -453,17 +453,15 @@ def request_body_size_within_limit(self): bool True if the request body size is within the limit, False otherwise. """ - headers = self.request.headers - - if "Content-Length" in headers: - content_length = int(headers["Content-Length"]) - - if content_length > self.max_request_size: - self.error_out( - 413, - info="Request Entity Too Large", - log_message=f"Request with size {content_length} exceeded limit of {self.max_request_size} (bytes).", - ) - return False - + if self.max_request_size is not None: + if "Content-Length" in self.request.headers: + content_length = int(self.request.headers["Content-Length"]) + if content_length > self.max_request_size: + self.error_out( + 413, + info="Request Entity Too Large", + log_message=f"Request with size {content_length} exceeded limit of {self.max_request_size} (bytes).", + ) + return False + return True From ca299adb8fd21cd2fbcd56c0e20b55bc61d4ddd1 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 18:07:15 -0700 Subject: [PATCH 10/21] Remove dev print statement. --- tabpy/tabpy_server/app/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index e646f8ba..3ac50193 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -121,7 +121,6 @@ def run(self): self.max_request_size = ( int(self.settings[SettingsParameters.MaxRequestSizeInMb]) * 1024 * 1024 ) - print(f"Setting max request size to {self.max_request_size} bytes") logger.info(f"Setting max request size to {self.max_request_size} bytes") init_model_evaluator(self.settings, self.tabpy_state, self.python_service) From 102feafb0786597c10faec77184f1fc8feb429c1 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 18:33:49 -0700 Subject: [PATCH 11/21] Standardize import order. --- tabpy/tabpy_server/app/app.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index 3ac50193..aeebf343 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -6,7 +6,13 @@ import shutil import signal import sys +import _thread + +import tornado +from tornado.http1connection import HTTP1Connection + import tabpy +import tabpy.tabpy_server.app.arrow_server as pa from tabpy.tabpy import __version__ from tabpy.tabpy_server.app.app_parameters import ConfigParameters, SettingsParameters from tabpy.tabpy_server.app.util import parse_pwd_file @@ -26,10 +32,6 @@ StatusHandler, UploadDestinationHandler, ) -import tornado -from tornado.http1connection import HTTP1Connection -import tabpy.tabpy_server.app.arrow_server as pa -import _thread logger = logging.getLogger(__name__) From 2a7958f1731913eff51283dbd0974d531c6fff00 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 18:34:02 -0700 Subject: [PATCH 12/21] Add max_request_size integ test. --- tests/integration/test_max_request_size.py | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/integration/test_max_request_size.py diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py new file mode 100644 index 00000000..808ec988 --- /dev/null +++ b/tests/integration/test_max_request_size.py @@ -0,0 +1,80 @@ +""" +Script evaluation tests. +""" + +from . import integ_test_base +import json +import gzip +import os +import requests +import string + + +class TestMaxRequestSize(integ_test_base.IntegTestBase): + def _get_config_file_name(self) -> str: + """ + Generates config file. Overwrite this function for tests to + run against not default state file. + + Returns + ------- + str + Absolute path to config file. + """ + config_file = open(os.path.join(self.tmp_dir, "test.conf"), "w+") + config_file.write( + "[TabPy]\n" + f"TABPY_QUERY_OBJECT_PATH = {self.tmp_dir}/query_objects\n" + f"TABPY_PORT = {self._get_port()}\n" + f"TABPY_GZIP_ENABLE = TRUE\n" + f"TABPY_STATE_PATH = {self.tmp_dir}\n" + "TABPY_MAX_REQUEST_SIZE_MB = 1\n" + ) + + pwd_file = self._get_pwd_file() + if pwd_file is not None: + pwd_file = os.path.abspath(pwd_file) + config_file.write(f"TABPY_PWD_FILE = {pwd_file}\n") + + transfer_protocol = self._get_transfer_protocol() + if transfer_protocol is not None: + config_file.write(f"TABPY_TRANSFER_PROTOCOL = {transfer_protocol}\n") + + cert_file_name = self._get_certificate_file_name() + if cert_file_name is not None: + cert_file_name = os.path.abspath(cert_file_name) + config_file.write(f"TABPY_CERTIFICATE_FILE = {cert_file_name}\n") + + key_file_name = self._get_key_file_name() + if key_file_name is not None: + key_file_name = os.path.abspath(key_file_name) + config_file.write(f"TABPY_KEY_FILE = {key_file_name}\n") + + evaluate_timeout = self._get_evaluate_timeout() + if evaluate_timeout is not None: + config_file.write(f"TABPY_EVALUATE_TIMEOUT = {evaluate_timeout}\n") + + config_file.close() + + self.delete_config_file = True + return config_file.name + + def test_payload_exceeds_max_request_size_evaluate(self): + size_mb = 2 + num_chars = size_mb * 1024 * 1024 + large_string = string.printable * (num_chars // len(string.printable)) + large_string += string.printable[:num_chars % len(string.printable)] + + payload = { + "data": { "_arg1": large_string }, + "script": "return _arg1" + } + headers = { + "Content-Type": "application/json", + } + url = self._get_url() + "/evaluate" + response = requests.request("POST", url, data=json.dumps(payload).encode('utf-8'), + headers=headers) + result = json.loads(response.text) + self.assertEqual(413, response.status_code) + From da7acfc19bcb987f56f4da2b518b6585b3ae614c Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 18:34:41 -0700 Subject: [PATCH 13/21] Add max request size test title. --- tests/integration/test_max_request_size.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py index 808ec988..5b96946b 100644 --- a/tests/integration/test_max_request_size.py +++ b/tests/integration/test_max_request_size.py @@ -1,5 +1,5 @@ """ -Script evaluation tests. +Max request size tests. """ from . import integ_test_base From 744eb04b72f5d53a1cd7fc2169766bd7361c75cd Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 18:52:10 -0700 Subject: [PATCH 14/21] Add max_request_size test for query endpoint. --- tests/integration/test_max_request_size.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py index 5b96946b..02e9083d 100644 --- a/tests/integration/test_max_request_size.py +++ b/tests/integration/test_max_request_size.py @@ -59,22 +59,26 @@ def _get_config_file_name(self) -> str: self.delete_config_file = True return config_file.name - def test_payload_exceeds_max_request_size_evaluate(self): + def create_large_payload(self): size_mb = 2 num_chars = size_mb * 1024 * 1024 large_string = string.printable * (num_chars // len(string.printable)) large_string += string.printable[:num_chars % len(string.printable)] - payload = { "data": { "_arg1": large_string }, "script": "return _arg1" } - headers = { - "Content-Type": "application/json", - } + return json.dumps(payload).encode('utf-8') + + def test_payload_exceeds_max_request_size_evaluate(self): + headers = { "Content-Type": "application/json" } url = self._get_url() + "/evaluate" - response = requests.request("POST", url, data=json.dumps(payload).encode('utf-8'), - headers=headers) - result = json.loads(response.text) + response = requests.post(url, data=self.create_large_payload(), headers=headers) + self.assertEqual(413, response.status_code) + + def test_payload_exceeds_max_request_size_query(self): + headers = { "Content-Type": "application/json" } + url = self._get_url() + "/query/model_name" + response = requests.post(url, data=self.create_large_payload(), headers=headers) self.assertEqual(413, response.status_code) From f4e0c02a6c550520aca9f0f9133306ffc2d1c9b4 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 19:21:33 -0700 Subject: [PATCH 15/21] Additionl test for missing content-length header --- tests/integration/test_max_request_size.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py index 02e9083d..74ea13f9 100644 --- a/tests/integration/test_max_request_size.py +++ b/tests/integration/test_max_request_size.py @@ -82,3 +82,12 @@ def test_payload_exceeds_max_request_size_query(self): response = requests.post(url, data=self.create_large_payload(), headers=headers) self.assertEqual(413, response.status_code) + def test_no_content_length_header_present(self): + headers = { "Content-Type": "application/json" } + url = self._get_url() + "/evaluate" + response = requests.post(url, headers=headers) + message = json.loads(response.text)["message"] + # Ensure it gets to processing message stage in EvaluationPlaneHandler.post + self.assertEqual("Error processing script", message) + self.assertEqual(500, response.status_code) + From add5bb21faf2f782ad133902442d7ab65af46854 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Sun, 21 May 2023 19:32:09 -0700 Subject: [PATCH 16/21] Add get query test to test_max_request_size. --- tests/integration/test_max_request_size.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py index 74ea13f9..587be899 100644 --- a/tests/integration/test_max_request_size.py +++ b/tests/integration/test_max_request_size.py @@ -81,13 +81,15 @@ def test_payload_exceeds_max_request_size_query(self): url = self._get_url() + "/query/model_name" response = requests.post(url, data=self.create_large_payload(), headers=headers) self.assertEqual(413, response.status_code) + response = requests.get(url, data=self.create_large_payload(), headers=headers) + self.assertEqual(413, response.status_code) def test_no_content_length_header_present(self): headers = { "Content-Type": "application/json" } url = self._get_url() + "/evaluate" response = requests.post(url, headers=headers) message = json.loads(response.text)["message"] - # Ensure it gets to processing message stage in EvaluationPlaneHandler.post + # Ensure it reaches script processing stage in EvaluationPlaneHandler.post self.assertEqual("Error processing script", message) self.assertEqual(500, response.status_code) From cb2acec5ee0bfe96c196d2df4b6784b383f1c449 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Mon, 22 May 2023 04:30:40 -0700 Subject: [PATCH 17/21] Move set max request size bytes to parse config. --- tabpy/tabpy_server/app/app.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index aeebf343..e0c74d13 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -120,11 +120,7 @@ def _get_arrow_server(self, config): def run(self): application = self._create_tornado_web_app() - self.max_request_size = ( - int(self.settings[SettingsParameters.MaxRequestSizeInMb]) * 1024 * 1024 - ) - logger.info(f"Setting max request size to {self.max_request_size} bytes") - + init_model_evaluator(self.settings, self.tabpy_state, self.python_service) protocol = self.settings[SettingsParameters.TransferProtocol] @@ -358,6 +354,12 @@ def _parse_config(self, config_file): ].lower() self._validate_transfer_protocol_settings() + + # Set max request size in bytes + self.max_request_size = ( + int(self.settings[SettingsParameters.MaxRequestSizeInMb]) * 1024 * 1024 + ) + logger.info(f"Setting max request size to {self.max_request_size} bytes") # if state.ini does not exist try and create it - remove # last dependence on batch/shell script From 650410b67b8769e37752fc7eadd25ab8673479c4 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Mon, 22 May 2023 04:44:11 -0700 Subject: [PATCH 18/21] Move tests to test_evaluation_plane_handler. --- tests/integration/test_max_request_size.py | 19 +----- .../test_evaluation_plane_handler.py | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py index 587be899..c3cb8f58 100644 --- a/tests/integration/test_max_request_size.py +++ b/tests/integration/test_max_request_size.py @@ -60,8 +60,7 @@ def _get_config_file_name(self) -> str: return config_file.name def create_large_payload(self): - size_mb = 2 - num_chars = size_mb * 1024 * 1024 + num_chars = 2 * 1024 * 1024 # 2MB Size large_string = string.printable * (num_chars // len(string.printable)) large_string += string.printable[:num_chars % len(string.printable)] payload = { @@ -70,12 +69,6 @@ def create_large_payload(self): } return json.dumps(payload).encode('utf-8') - def test_payload_exceeds_max_request_size_evaluate(self): - headers = { "Content-Type": "application/json" } - url = self._get_url() + "/evaluate" - response = requests.post(url, data=self.create_large_payload(), headers=headers) - self.assertEqual(413, response.status_code) - def test_payload_exceeds_max_request_size_query(self): headers = { "Content-Type": "application/json" } url = self._get_url() + "/query/model_name" @@ -83,13 +76,3 @@ def test_payload_exceeds_max_request_size_query(self): self.assertEqual(413, response.status_code) response = requests.get(url, data=self.create_large_payload(), headers=headers) self.assertEqual(413, response.status_code) - - def test_no_content_length_header_present(self): - headers = { "Content-Type": "application/json" } - url = self._get_url() + "/evaluate" - response = requests.post(url, headers=headers) - message = json.loads(response.text)["message"] - # Ensure it reaches script processing stage in EvaluationPlaneHandler.post - self.assertEqual("Error processing script", message) - self.assertEqual(500, response.status_code) - diff --git a/tests/unit/server_tests/test_evaluation_plane_handler.py b/tests/unit/server_tests/test_evaluation_plane_handler.py index dba0e5ea..e6aba894 100755 --- a/tests/unit/server_tests/test_evaluation_plane_handler.py +++ b/tests/unit/server_tests/test_evaluation_plane_handler.py @@ -1,6 +1,8 @@ import base64 +import json import os import tempfile +import string from tornado.testing import AsyncHTTPTestCase @@ -458,6 +460,67 @@ def test_evaluation_enabled(self): ) self.assertEqual(200, response.code) +class TestEvaluationPlaneHandlerMaxRequestSize(AsyncHTTPTestCase): + @classmethod + def setUpClass(cls): + prefix = "__TestEvaluationPlaneHandlerMaxRequestSize_" + + # create config file + cls.config_file = tempfile.NamedTemporaryFile( + mode="w+t", prefix=prefix, suffix=".conf", delete=False + ) + cls.config_file.write( + "[TabPy]\n" + "TABPY_MAX_REQUEST_SIZE_MB = 1" + ) + cls.config_file.close() + + @classmethod + def tearDownClass(cls): + os.remove(cls.config_file.name) + + def get_app(self): + self.app = TabPyApp(self.config_file.name) + return self.app._create_tornado_web_app() + + def create_large_payload(self): + num_chars = 2 * 1024 * 1024 # 2MB Size + large_string = string.printable * (num_chars // len(string.printable)) + large_string += string.printable[:num_chars % len(string.printable)] + payload = { + "data": { "_arg1": [1, large_string] }, + "script": "return _arg1" + } + return json.dumps(payload).encode('utf-8') + + def test_evaluation_payload_exceeds_max_request_size(self): + response = self.fetch( + "/evaluate", + method="POST", + body=self.create_large_payload() + ) + self.assertEqual(413, response.code) + + def test_evaluation_max_request_size_not_applied(self): + self.app.max_request_size = None + response = self.fetch( + "/evaluate", + method="POST", + body=self.create_large_payload() + ) + self.assertEqual(200, response.code) + self.assertEqual(1, json.loads(response.body)[0]) + + def test_no_content_length_header_present(self): + response = self.fetch( + "/evaluate", + method="POST", + allow_nonstandard_methods=True + ) + message = json.loads(response.body)["message"] + # Ensure it reaches script processing stage in EvaluationPlaneHandler.post + self.assertEqual("Error processing script", message) + class TestEvaluationPlaneHandlerDefault(AsyncHTTPTestCase): @classmethod From b5fe7fef71a8206e53f548f51243f896cb1d0a04 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Mon, 22 May 2023 04:55:13 -0700 Subject: [PATCH 19/21] Remove query endpoint test. --- tests/integration/test_max_request_size.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py index c3cb8f58..79884d24 100644 --- a/tests/integration/test_max_request_size.py +++ b/tests/integration/test_max_request_size.py @@ -69,10 +69,10 @@ def create_large_payload(self): } return json.dumps(payload).encode('utf-8') - def test_payload_exceeds_max_request_size_query(self): - headers = { "Content-Type": "application/json" } - url = self._get_url() + "/query/model_name" - response = requests.post(url, data=self.create_large_payload(), headers=headers) - self.assertEqual(413, response.status_code) - response = requests.get(url, data=self.create_large_payload(), headers=headers) - self.assertEqual(413, response.status_code) + # def test_payload_exceeds_max_request_size_query(self): + # headers = { "Content-Type": "application/json" } + # url = self._get_url() + "/query/model_name" + # response = requests.post(url, data=self.create_large_payload(), headers=headers) + # self.assertEqual(413, response.status_code) + # response = requests.get(url, data=self.create_large_payload(), headers=headers) + # self.assertEqual(413, response.status_code) From dd05d9d3b8ba8b81935441e9df813fec11842c80 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Mon, 22 May 2023 05:04:01 -0700 Subject: [PATCH 20/21] Delete unused test_max_request_size file. --- tests/integration/test_max_request_size.py | 78 ---------------------- 1 file changed, 78 deletions(-) delete mode 100644 tests/integration/test_max_request_size.py diff --git a/tests/integration/test_max_request_size.py b/tests/integration/test_max_request_size.py deleted file mode 100644 index 79884d24..00000000 --- a/tests/integration/test_max_request_size.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Max request size tests. -""" - -from . import integ_test_base -import json -import gzip -import os -import requests -import string - - -class TestMaxRequestSize(integ_test_base.IntegTestBase): - def _get_config_file_name(self) -> str: - """ - Generates config file. Overwrite this function for tests to - run against not default state file. - - Returns - ------- - str - Absolute path to config file. - """ - config_file = open(os.path.join(self.tmp_dir, "test.conf"), "w+") - config_file.write( - "[TabPy]\n" - f"TABPY_QUERY_OBJECT_PATH = {self.tmp_dir}/query_objects\n" - f"TABPY_PORT = {self._get_port()}\n" - f"TABPY_GZIP_ENABLE = TRUE\n" - f"TABPY_STATE_PATH = {self.tmp_dir}\n" - "TABPY_MAX_REQUEST_SIZE_MB = 1\n" - ) - - pwd_file = self._get_pwd_file() - if pwd_file is not None: - pwd_file = os.path.abspath(pwd_file) - config_file.write(f"TABPY_PWD_FILE = {pwd_file}\n") - - transfer_protocol = self._get_transfer_protocol() - if transfer_protocol is not None: - config_file.write(f"TABPY_TRANSFER_PROTOCOL = {transfer_protocol}\n") - - cert_file_name = self._get_certificate_file_name() - if cert_file_name is not None: - cert_file_name = os.path.abspath(cert_file_name) - config_file.write(f"TABPY_CERTIFICATE_FILE = {cert_file_name}\n") - - key_file_name = self._get_key_file_name() - if key_file_name is not None: - key_file_name = os.path.abspath(key_file_name) - config_file.write(f"TABPY_KEY_FILE = {key_file_name}\n") - - evaluate_timeout = self._get_evaluate_timeout() - if evaluate_timeout is not None: - config_file.write(f"TABPY_EVALUATE_TIMEOUT = {evaluate_timeout}\n") - - config_file.close() - - self.delete_config_file = True - return config_file.name - - def create_large_payload(self): - num_chars = 2 * 1024 * 1024 # 2MB Size - large_string = string.printable * (num_chars // len(string.printable)) - large_string += string.printable[:num_chars % len(string.printable)] - payload = { - "data": { "_arg1": large_string }, - "script": "return _arg1" - } - return json.dumps(payload).encode('utf-8') - - # def test_payload_exceeds_max_request_size_query(self): - # headers = { "Content-Type": "application/json" } - # url = self._get_url() + "/query/model_name" - # response = requests.post(url, data=self.create_large_payload(), headers=headers) - # self.assertEqual(413, response.status_code) - # response = requests.get(url, data=self.create_large_payload(), headers=headers) - # self.assertEqual(413, response.status_code) From 647f2a64fda5de0f62a15217b2595b945afe50b7 Mon Sep 17 00:00:00 2001 From: Jake Ichikawa Date: Tue, 23 May 2023 05:22:02 -0700 Subject: [PATCH 21/21] Update changelog for 2.8.0. --- CHANGELOG | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 83a30ea0..6033c151 100755 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ # Changelog +## v2.8.0 + +### Improvements + +- Returns 413 error code when request payload exceeds +TABPY_MAX_REQUEST_SIZE_MB config setting. + ## v2.7.0 ### Improvements