Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ 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:
app = StaticRewriteMiddleware(app, static=static)
# If you're using Starlette / FastAPI / Air, add the middleware to your app like this:
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.
Expand Down
6 changes: 3 additions & 3 deletions src/staticware/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 ────────────────────────────────────────────────────
Expand Down
55 changes: 0 additions & 55 deletions tests/test_staticware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<html>hello</html>"
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