From 0b471c8c2f8a82d23e90f9b59ee6fec738565239 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Sun, 19 Jul 2026 12:14:14 +0000 Subject: [PATCH] Add web_application_options setting to configure the web application (#551) The aiohttp web driver always constructed aiohttp.web.Application() with no arguments, so there was no way to set things like client_max_size or install middlewares on the embedded web server. Add a new web_application_options mapping setting whose contents are forwarded as keyword arguments to the web framework's application object. For the default aiohttp driver these go straight to aiohttp.web.Application(**options). Defaults to None (empty), preserving the previous behaviour. Closes #551. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HHPL4VFWQRQPpjR1gXSKyL --- faust/types/settings/settings.py | 29 ++++++++++++++++++++++++++ faust/web/drivers/aiohttp.py | 4 +++- tests/functional/test_app.py | 4 ++++ tests/unit/web/drivers/test_aiohttp.py | 20 ++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/faust/types/settings/settings.py b/faust/types/settings/settings.py index 9a259b8dd..cf080dab8 100644 --- a/faust/types/settings/settings.py +++ b/faust/types/settings/settings.py @@ -158,6 +158,7 @@ def __init__( canonical_url: URLArg = None, web: URLArg = None, web_bind: Optional[str] = None, + web_application_options: typing.Mapping[str, typing.Any] = None, web_cors_options: typing.Mapping[str, ResourceOptions] = None, web_enabled: Optional[bool] = None, web_host: Optional[str] = None, @@ -1857,6 +1858,34 @@ def web_bind(self) -> str: not by passing it as a keyword argument to :class:`app`. """ + @sections.WebServer.setting( + params.Dict[Any], + version_introduced="0.11.4", + ) + def web_application_options(self) -> Mapping[str, Any]: + """Extra keyword arguments passed to the web framework's application. + + Use this to configure the underlying web application object that the + web driver creates. For the default :pypi:`aiohttp` driver these are + forwarded straight to :class:`aiohttp.web.Application`, so you can set + things like ``client_max_size`` or install middlewares: + + .. sourcecode:: python + + from aiohttp.web import middleware + + @middleware + async def error_middleware(request, handler): + ... + + app = App(..., web_application_options={ + 'client_max_size': 1024 ** 2 * 20, + 'middlewares': [error_middleware], + }) + + The accepted keys depend on the configured web driver. + """ + @sections.WebServer.setting( params.Dict[ResourceOptions], version_introduced="1.5", diff --git a/faust/web/drivers/aiohttp.py b/faust/web/drivers/aiohttp.py index 16c0d4fde..3eff5a609 100644 --- a/faust/web/drivers/aiohttp.py +++ b/faust/web/drivers/aiohttp.py @@ -89,7 +89,9 @@ class Web(base.Web): def __init__(self, app: AppT, **kwargs: Any) -> None: super().__init__(app, **kwargs) - self.web_app: Application = Application() + self.web_app: Application = Application( + **(app.conf.web_application_options or {}) + ) self.cors_options = _prepare_cors_options(app.conf.web_cors_options or {}) self._runner: AppRunner = AppRunner(self.web_app, access_log=None) self._transport_handlers = { diff --git a/tests/functional/test_app.py b/tests/functional/test_app.py index 94ebe8b86..1d27c1b7b 100644 --- a/tests/functional/test_app.py +++ b/tests/functional/test_app.py @@ -552,6 +552,7 @@ def test_defaults(self): assert conf.web_bind == "0.0.0.0" assert conf.web_port == 6066 assert conf.web_transport == Settings.web_transport.default + assert conf.web_application_options is None assert conf.web_cors_options is None assert conf.worker_redirect_stdouts assert conf.worker_redirect_stdouts_level == "WARN" @@ -666,6 +667,7 @@ def assert_config_equivalent( web_host="localhost", web_transport="udp://", web_in_thread=True, + web_application_options={"client_max_size": 1024}, # noqa: B006 web_cors_options={ # noqa: B006 "http://example.com": ResourceOptions( # noqa: B008 allow_credentials=True, @@ -742,6 +744,7 @@ def assert_config_equivalent( web_host=web_host, web_transport=web_transport, web_in_thread=web_in_thread, + web_application_options=web_application_options, web_cors_options=web_cors_options, worker_redirect_stdouts=worker_redirect_stdouts, worker_redirect_stdouts_level=worker_redirect_stdouts_level, @@ -806,6 +809,7 @@ def assert_config_equivalent( assert conf.web_port == web_port assert conf.web_host == web_host assert conf.web_transport == URL(web_transport) + assert conf.web_application_options == web_application_options assert conf.web_cors_options == web_cors_options assert conf.worker_redirect_stdouts == worker_redirect_stdouts assert conf.worker_redirect_stdouts_level == worker_redirect_stdouts_level diff --git a/tests/unit/web/drivers/test_aiohttp.py b/tests/unit/web/drivers/test_aiohttp.py index 1748f1363..a8eac2d0b 100644 --- a/tests/unit/web/drivers/test_aiohttp.py +++ b/tests/unit/web/drivers/test_aiohttp.py @@ -14,6 +14,7 @@ NON_OPTIONS_METHODS, Server, ServerThread, + Web, _prepare_cors_options, ) from tests.helpers import AsyncMock @@ -127,6 +128,25 @@ async def test_on_stop(self, *, server, web): class Test_Web: + @pytest.mark.conf(web_application_options={"client_max_size": 1234}) + def test_web_application_options(self, *, app): + # web_application_options are forwarded to aiohttp.web.Application. + # See issue #551. + with ( + patch("faust.web.drivers.aiohttp.Application") as Application, + patch("faust.web.drivers.aiohttp.AppRunner"), + ): + Web(app) + Application.assert_called_once_with(client_max_size=1234) + + def test_web_application_options__default(self, *, app): + with ( + patch("faust.web.drivers.aiohttp.Application") as Application, + patch("faust.web.drivers.aiohttp.AppRunner"), + ): + Web(app) + Application.assert_called_once_with() + def test_cors(self, *, web): assert web.cors is web.cors assert isinstance(web.cors, aiohttp_cors.CorsConfig)