starlette_exporter icon indicating copy to clipboard operation
starlette_exporter copied to clipboard

Starlette_exporter ignores responses from other middleware

Open lainiwa opened this issue 6 months ago • 1 comments

I use starlette_exporter alonside asgi-ratelimit, and it seems starlette_exporter ignores the HTTP 429 responses from that ratelimiting middleware.

Not sure about the root cause, but I created a dummy middleware that successfully catches and prints those 429 responses. That's why I decided it is more likely to be a starlette_exporter peculiarity.

To reproduce:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from ratelimit import RateLimitMiddleware, Rule
from ratelimit.backends.simple import MemoryBackend
from ratelimit.types import Scope
from starlette.middleware.base import BaseHTTPMiddleware
from starlette_exporter import PrometheusMiddleware, handle_metrics

app = FastAPI()


async def _dummy_auth_function(scope: Scope) -> tuple[str, str]:
    return "dummy_uid", "default"

app.add_middleware(
    RateLimitMiddleware,
    authenticate=_dummy_auth_function,
    backend=MemoryBackend(),
    config={
        r"^/test/ratelimited": [Rule(minute=1)],
    },
)

app.add_middleware(
    PrometheusMiddleware,
    filter_unhandled_paths=True,
)

app.add_route("/metrics", handle_metrics)


class StatusCodeLoggerMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)
        print(f"Returned status code: {response.status_code}")
        return response


app.add_middleware(StatusCodeLoggerMiddleware)


@app.get("/test/ratelimited")
async def test_ratelimited():
    return {"message": "Hello World"}


@app.get("/test/failing")
async def test_failing():
    return JSONResponse(status_code=429, content={"message": "Too many requests"})


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, port=8001, log_config=None)

Now if we query it:

curl -Sv http://localhost:8001/test/ratelimited 2>&1 |grep '< HTTP'  # HTTP 200
curl -Sv http://localhost:8001/test/ratelimited 2>&1 |grep '< HTTP'  # HTTP 429

We would not see anything 429-related in the metrics:

curl -sS http://localhost:8001/metrics |grep 429  # nothing

However we can see 429 status codes, if they were returned from the endpoint function itself:

curl -Sv http://localhost:8001/test/failing 2>&1 |grep '< HTTP'  # HTTP 429
curl -sS http://localhost:8001/metrics |grep 429  # 20 rows of starlette_request... metrics

lainiwa avatar Aug 13 '24 15:08 lainiwa