diff --git a/CHANGELOG.md b/CHANGELOG.md index f04774ec..2c20bd9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Cleanup: deprecate config option 'ldap_use_ssl' for good * Performance: improve `path_to_filesystem()` * Performance: preload access rights from file +* Add: [server] delay_on_error option ## 3.6.1 diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index afaa5daf..9e910026 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -831,6 +831,14 @@ The maximum number of parallel connections. Set to `0` to disable the limit. Default: `8` +##### delay_on_error + +_(>= 3.7.0)_ + +Base delay in case of error response (seconds) + +Default: `0.01` + ##### max_content_length The maximum size of the request body. (bytes) diff --git a/config b/config index 4b157fc7..86363049 100644 --- a/config +++ b/config @@ -21,6 +21,9 @@ # Max parallel connections #max_connections = 8 +# Base delay in case of error response (seconds) +#delay_on_error = 0.01 + # Max size of request body (bytes), default: 100 Mbyte # In case of using a reverse proxy in front of check also there related option #max_content_length = 100000000 diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index 54016c81..e52934dd 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -3,7 +3,7 @@ # Copyright © 2008 Pascal Halter # Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2017-2019 Unrud -# Copyright © 2024-2025 Peter Bieringer +# Copyright © 2024-2026 Peter Bieringer # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -72,6 +72,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead, _mask_passwords: bool _auth_delay: float + _delay_on_error: float _internal_server: bool _max_content_length: int _max_resource_size: int @@ -97,6 +98,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead, """ super().__init__(configuration) self._mask_passwords = configuration.get("logging", "mask_passwords") + self._delay_on_error = configuration.get("server", "delay_on_error") + logger.info("delay_on_error set to: %.3f seconds", self._delay_on_error) self._max_content_length = configuration.get("server", "max_content_length") self._max_resource_size = configuration.get("server", "max_resource_size") logger.info("max_content_length set to: %d bytes (%sbytes)", self._max_content_length, utils.format_unit(self._max_content_length, binary=True)) @@ -105,7 +108,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead, logger.warning("max_resource_size set to: %d bytes (%sbytes) (capped from %d to 80%% of max_content_length)", max_resource_size_limited, utils.format_unit(max_resource_size_limited, binary=True), self._max_resource_size) self._max_resource_size = max_resource_size_limited else: - logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True)) + logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True)) self._bad_put_request_content = configuration.get("logging", "bad_put_request_content") logger.info("log bad put request content: %s", self._bad_put_request_content) self._request_header_on_debug = configuration.get("logging", "request_header_on_debug") @@ -304,6 +307,15 @@ class Application(ApplicationPartDelete, ApplicationPartHead, flags_text = " (" + " ".join(flags) + ")" else: flags_text = "" + # delay on error + if status >= 400: + if self._delay_on_error > 0: + random_delay = self._delay_on_error * (1 + random.random()) + if status >= 500: + random_delay = 2 * random_delay + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Response delay triggered by result code: %d -> %0.3f seconds", status, random_delay) + time.sleep(random_delay) if answer is not None: logger.info("%s response status for %r%s in %.3f seconds %s %s bytes%s: %s", request_method, unsafe_path, depthinfo, diff --git a/radicale/config.py b/radicale/config.py index bf04d7f4..76e4fdbd 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -171,6 +171,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "value": "100000000", "help": "maximum size of request body in bytes (default: 100 Mbyte)", "type": positive_int}), + ("delay_on_error", { + "value": "0.01", + "help": "base delay in case of error response (seconds)", + "type": positive_float}), ("max_resource_size", { "value": "10000000", "help": "maximum size of resource (default: 10 Mbyte)", diff --git a/radicale/tests/test_auth.py b/radicale/tests/test_auth.py index daf735aa..b66f3ef4 100644 --- a/radicale/tests/test_auth.py +++ b/radicale/tests/test_auth.py @@ -69,8 +69,10 @@ class TestBaseAuthRequests(BaseTest): with open(htpasswd_file_path, "w", encoding=encoding) as f: f.write(htpasswd_content) self.configure({"auth": {"type": "htpasswd", + "delay": 0, "htpasswd_filename": htpasswd_file_path, - "htpasswd_encryption": htpasswd_encryption}}) + "htpasswd_encryption": htpasswd_encryption}, + "server": {"delay_on_error": 0}}) if test_matrix == "ascii": test_matrix = (("tmp", "bepo", True), ("tmp", "tmp", False), ("tmp", "", False), ("unk", "unk", False), diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 4eee5c05..7f37542e 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -73,6 +73,7 @@ collection: .* permissions: RrWw""") self.configure({"rights": {"file": rights_file_path, "type": "from_file"}, + "server": {"delay_on_error": 0}, "logging": {"request_header_on_debug": "True", "request_content_on_debug": "True", "response_header_on_debug": "True",