From d57d6b9f06e3cdadf0084a458cc833ddddb99ac1 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Fri, 13 Mar 2026 14:43:22 +0800 Subject: [PATCH 1/2] Follow starlette guide for middleware I told @audreyfeldroy that the `__call__` method needed to return an app. That was wrong and she made commits based on my error. This PR addresses my mistake and returns the project to her original design. --- README.md | 2 +- docs/usage.md | 2 +- src/staticware/middleware.py | 6 ++-- tests/test_staticware.py | 55 ------------------------------------ 4 files changed, 5 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index ba57728..f3d0a41 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ static = HashedStatic("static") app.mount("/static", static) # Wrap any ASGI app to rewrite static paths in HTML responses -app = StaticRewriteMiddleware(app, static=static) +app.add_middleware(StaticRewriteMiddleware, static=static) # In templates, resolve cache-busted URLs: static.url("styles.css") # /static/styles.a1b2c3d4.css diff --git a/docs/usage.md b/docs/usage.md index ea313f3..05a921e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -13,7 +13,7 @@ static = HashedStatic("static") app.mount("/static", static) # Wrap the app to rewrite static paths in HTML responses: -app = StaticRewriteMiddleware(app, static=static) +app.add_middleware(StaticRewriteMiddleware, static=static) ``` `HashedStatic` hashes every file in the directory at startup. When a browser requests the hashed filename, it gets an immutable cache header. When it requests the original filename, the file is served without aggressive caching. diff --git a/src/staticware/middleware.py b/src/staticware/middleware.py index 68cdf48..fe0447d 100644 --- a/src/staticware/middleware.py +++ b/src/staticware/middleware.py @@ -8,7 +8,7 @@ static = HashedStatic("static") # Wrap any ASGI app to rewrite /static/styles.css -> /static/styles.a1b2c3d4.css - app = StaticRewriteMiddleware(your_app, static=static) + app.add_middleware(StaticRewriteMiddleware, static=static) # In templates: static.url("styles.css") # -> /static/styles.a1b2c3d4.css @@ -184,7 +184,7 @@ class StaticRewriteMiddleware: HTML — no template function needed (though ``static.url()`` is there if you want it). - app = StaticRewriteMiddleware(app, static=static) + app.add_middleware(StaticRewriteMiddleware, static=static) """ def __init__(self, app: ASGIApp, *, static: HashedStatic) -> None: @@ -251,7 +251,7 @@ async def send_wrapper(message: dict[str, Any]) -> None: await send({"type": "http.response.body", "body": full_body}) return - return await self.app(scope, receive, send_wrapper) + await self.app(scope, receive, send_wrapper) # ── Raw ASGI helpers ──────────────────────────────────────────────────── diff --git a/tests/test_staticware.py b/tests/test_staticware.py index 09bdc2f..bb434e8 100644 --- a/tests/test_staticware.py +++ b/tests/test_staticware.py @@ -526,58 +526,3 @@ async def test_hashed_url_no_etag(static: HashedStatic) -> None: await static(make_scope(f"/static/{hashed_name}"), receive, resp) assert resp.status == 200 assert b"etag" not in resp.headers, "Hashed URL should not include an etag header" - - -# ── StaticRewriteMiddleware: return value propagation ────────────── - - -async def test_rewrite_middleware_returns_inner_app_result( - static: HashedStatic, -) -> None: - """Middleware should propagate the inner app's return value on the HTTP path. - - ASGI apps normally return None, but the spec does not forbid return values. - Frameworks like Starlette rely on ``return await self.app(...)`` so that - return values propagate through the middleware chain. A bare ``await`` - without ``return`` silently discards the result. - """ - sentinel = "app_result" - - async def inner_app(scope: dict, receive: Any, send: Any) -> str: - body = b"hello" - await send( - { - "type": "http.response.start", - "status": 200, - "headers": [ - (b"content-type", b"text/html; charset=utf-8"), - (b"content-length", str(len(body)).encode("latin-1")), - ], - } - ) - await send({"type": "http.response.body", "body": body}) - return sentinel - - app = StaticRewriteMiddleware(inner_app, static=static) - resp = ResponseCollector() - result = await app(make_scope("/"), receive, resp) - assert result == sentinel - - -async def test_rewrite_middleware_returns_inner_app_result_non_http( - static: HashedStatic, -) -> None: - """Middleware should propagate the inner app's return value for non-HTTP scopes. - - When the scope type is not "http", the middleware forwards directly to the - inner app. It must ``return await self.app(...)`` so the return value is - not silently discarded. - """ - sentinel = "ws_result" - - async def inner_app(scope: dict, receive: Any, send: Any) -> str: - return sentinel - - app = StaticRewriteMiddleware(inner_app, static=static) - result = await app({"type": "websocket", "path": "/"}, receive, ResponseCollector()) - assert result == sentinel From be83fa3f7c9785bcee27c95507d0fcd7553ffde9 Mon Sep 17 00:00:00 2001 From: "Audrey M. Roy Greenfeld" Date: Fri, 13 Mar 2026 14:48:49 +0800 Subject: [PATCH 2/2] Apply suggestion from @audreyfeldroy Signed-off-by: Audrey M. Roy Greenfeld --- docs/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index 05a921e..89aea91 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -12,7 +12,7 @@ static = HashedStatic("static") # Mount it however your framework mounts sub-apps: app.mount("/static", static) -# Wrap the app to rewrite static paths in HTML responses: +# If you're using Starlette / FastAPI / Air, add the middleware to your app like this: app.add_middleware(StaticRewriteMiddleware, static=static) ```