diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index db5cc69b03..8e2130fb88 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -15,6 +15,10 @@ All notable changes to the **Prowler API** are documented in this file. - Gunicorn worker timeout raised from the 30s default to 120s, so long-running requests are no longer killed prematurely [(#11631)](https://github.com/prowler-cloud/prowler/pull/11631) - Sentry now drops ASGI's `RequestAborted` errors from health-check probe disconnects on `/health/live` [(#11632)](https://github.com/prowler-cloud/prowler/pull/11632) +### 🐞 Fixed + +- Database connections no longer leak under the ASGI worker, which previously exhausted the read replica's connection slots and caused 500s on read endpoints [(#11640)](https://github.com/prowler-cloud/prowler/pull/11640) + ### 🔐 Security - `aiohttp` to 3.14.0 and `idna` to 3.15, patching known CVEs [(#11596)](https://github.com/prowler-cloud/prowler/pull/11596) diff --git a/api/src/backend/api/middleware.py b/api/src/backend/api/middleware.py index 63f2fc630b..2b0a2340c4 100644 --- a/api/src/backend/api/middleware.py +++ b/api/src/backend/api/middleware.py @@ -1,9 +1,35 @@ import logging import time +from django.core.handlers.asgi import ASGIRequest +from django.db import connections + from config.custom_logging import BackendLogger +class CloseDBConnectionsMiddleware: + """ + Close request-scoped DB connections at the end of each ASGI request. + + Under the ASGI worker, connections opened by sync views are not released + by Django's normal request-boundary cleanup, so they accumulate idle until + Postgres runs out of slots. Only ASGI requests are handled; the sync WSGI + test client manages its own connections and must be left alone. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + try: + return self.get_response(request) + finally: + if isinstance(request, ASGIRequest): + for conn in connections.all(initialized_only=True): + if not conn.in_atomic_block: + conn.close_if_unusable_or_obsolete() + + def extract_auth_info(request) -> dict: if getattr(request, "auth", None) is not None: tenant_id = request.auth.get("tenant_id", "N/A") diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 3308561b48..31a9537b5f 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -49,6 +49,7 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ + "api.middleware.CloseDBConnectionsMiddleware", "django_guid.middleware.guid_middleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",