Skip to content

ETag Middleware

A lightweight production-ready ETag middleware for the sillo ASGI framework.

A lightweight, production‑ready ETag middleware for the sillo ASGI framework.

It automatically:

  • Computes and sets an ETag header for responses that don’t already have one
  • Handles conditional GET/HEAD requests via If-None-Match and returns 304 Not Modified when appropriate
Terminal window
uv add sillo-contrib
from sillo import silloApp
import sillo_contrib.etag as etag
app = silloApp()
# Add the ETag middleware (defaults shown)
app.use(
etag.ETag(
weak=True, # Generate weak validators (W/ prefixes)
methods=("GET", "HEAD"), # Methods to apply to
override=False # Don't overwrite an existing ETag by default
)
)
@app.get("/")
async def home(request, response):
return {"message": "Hello with ETag!"}

That’s it! Responses to GET/HEAD will carry an ETag header. Clients sending If-None-Match will receive 304 when the ETag matches.

  • weak: bool = True
    Use weak validators (e.g. W/"abc"). Set False for strong validators when you know the body bytes are stable across platforms and encodings.

  • methods: Iterable[str] = ("GET", "HEAD")
    Limit conditional handling to idempotent methods. You can add others, but it’s not typical.

  • override: bool = False
    If True, overwrites an ETag already set by your handler.

  1. If the handler doesn’t set an ETag, the middleware computes one from the response body and sets it
  2. If the request includes If-None-Match and it matches the response’s ETag (weak compare by default), the middleware converts the response into 304 Not Modified and removes the body, per RFC 9110
from sillo import silloApp
import sillo_contrib.etag as etag
app = silloApp()
app.use(etag.ETag())
@app.get("/api/data")
async def get_data(request, response):
# This response will automatically get an ETag
return {"data": "some content", "timestamp": "2024-01-01"}
# First request: Returns 200 with ETag: W/"abc123"
# Second request with If-None-Match: W/"abc123": Returns 304 Not Modified
app.use(
etag.ETag(
weak=False, # Use strong validators
methods=("GET", "HEAD")
)
)
@app.get("/api/file")
async def get_file(request, response):
# Strong ETag will be generated: "abc123" (no W/ prefix)
return {"file_content": "binary data"}
app.use(
etag.ETag(
methods=("GET", "HEAD", "POST"), # Include POST (not recommended)
override=True # Override existing ETags
)
)
  • Use for idempotent methods: Apply by default to GET and HEAD. Avoid applying to mutating methods like POST/PUT unless you know what you’re doing
  • Consider response size: Streaming or extremely large bodies may benefit from precomputed/strong ETags at your handler or via a hash of a stable resource version
  • Weak vs Strong: Use weak ETags (default) unless you need byte-for-byte identical responses
  • Cache-friendly: ETags work great with HTTP caching - browsers and CDNs will use them automatically
@app.get("/api/custom")
async def custom_etag(request, response):
# Set your own ETag - middleware won't override by default
response.headers['ETag'] = 'W/"custom-etag-123"'
return {"data": "custom content"}
@app.get("/api/conditional")
async def conditional_response(request, response):
# Check if client has current version
if_none_match = request.headers.get('If-None-Match')
current_etag = 'W/"version-456"'
if if_none_match == current_etag:
# Client has current version
response.status_code = 304
return None
# Return new content
response.headers['ETag'] = current_etag
return {"data": "updated content"}
  • When returning 304, the middleware ensures the body is empty and relies on the underlying response type to handle headers correctly
  • ETags are computed from the response body, so identical content will have identical ETags
  • The middleware respects existing ETags unless override=True is set
  • ETag computation adds minimal overhead for small responses
  • For large responses, consider pre-computing ETags based on resource versions
  • ETags enable efficient caching, reducing server load for repeated requests
  • Use with appropriate Cache-Control headers for optimal caching behavior

Built with ❤️ by the @sillohq community.