You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/05/04 14:53:56 UTC
(tooling-trusted-releases) branch sbp updated: Wait longer for uploads to complete
This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new d81b2def Wait longer for uploads to complete
d81b2def is described below
commit d81b2def27cb3471efb03bdeb493e24d54f54ebe
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Mon May 4 15:53:33 2026 +0100
Wait longer for uploads to complete
---
atr/config.py | 2 ++
atr/server.py | 24 ++++++++++++++++++++++++
2 files changed, 26 insertions(+)
diff --git a/atr/config.py b/atr/config.py
index 2bf8f47d..a7043cda 100644
--- a/atr/config.py
+++ b/atr/config.py
@@ -117,6 +117,8 @@ class AppConfig:
APACHE_RAT_JAR_PATH = decouple.config("APACHE_RAT_JAR_PATH", default=f"/opt/tools/apache-rat-{_RAT_VERSION}.jar")
# Maximum content length for requests
MAX_CONTENT_LENGTH: int = decouple.config("MAX_CONTENT_LENGTH", default=512 * _MB, cast=int)
+ # Maximum duration to receive upload request bodies
+ UPLOAD_BODY_TIMEOUT: int = decouple.config("UPLOAD_BODY_TIMEOUT", default=3600, cast=int)
# Maximum size limit for archive extraction
MAX_EXTRACT_SIZE: int = decouple.config("MAX_EXTRACT_SIZE", default=2 * _GB, cast=int)
# Chunk size for reading files during extraction
diff --git a/atr/server.py b/atr/server.py
index 7de0be3b..1640876d 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -483,6 +483,8 @@ def _app_setup_request_lifecycle(app: base.QuartApp) -> None:
logger = structlog.get_logger("atr.request")
+ app.before_request(_apply_upload_body_timeout)
+
@app.before_request
async def bind_request_context_vars() -> None:
await _reset_request_log_context()
@@ -633,6 +635,12 @@ async def _app_shutdown_log_listeners(app):
listener.stop()
+async def _apply_upload_body_timeout() -> None:
+ if not _uses_upload_body_timeout(quart.request.method, quart.request.path):
+ return
+ quart.request.body_timeout = config.get().UPLOAD_BODY_TIMEOUT
+
+
async def _backfill_archive_cache() -> None:
backfill_results = await asyncio.to_thread(quarantine.backfill_archive_cache)
if backfill_results:
@@ -1021,6 +1029,16 @@ def _register_routes(app: base.QuartApp) -> None: # noqa: C901
return quart.jsonify({"error": "413 Payload Too Large"}), 413
return await template.render("error.html", error="413 Payload Too Large", traceback="", status_code=413), 413
+ @app.errorhandler(408)
+ async def handle_request_timeout(error: Exception) -> Any:
+ timeout = quart.request.body_timeout
+ log.warning(f"Request timed out while reading request body after {timeout} seconds")
+ message = f"408 Request Timeout: request body did not finish before the server timeout of {timeout} seconds."
+ wants_json = quart.request.accept_mimetypes.best_match(["application/json", "text/html"]) == "application/json"
+ if quart.request.path.startswith("/api") or wants_json:
+ return quart.jsonify({"error": message}), 408
+ return await template.render("error.html", error=message, traceback="", status_code=408), 408
+
# Add a global error handler in case a page does not exist.
@app.errorhandler(404)
async def handle_not_found(error: Exception) -> Any:
@@ -1078,6 +1096,12 @@ def _set_file_permissions_to_read_only() -> None:
log.info(f"Set permissions of {fixed_count} files to read only (0o444)")
+def _uses_upload_body_timeout(method: str, path: str) -> bool:
+ if method != "POST":
+ return False
+ return path.startswith("/upload/") or (path == "/api/release/upload")
+
+
def _validate_config(app_config: type[config.AppConfig], hot_reload: bool) -> None:
config.validate()
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org