GET /rounds/stream capped concurrent subscribers with one global counter (MAX_SUBSCRIBERS=500): anyone opening 500 connections degraded every other user to polling. The comment called it a defensive cap; it was actually the vector, since nothing stopped a single source from exhausting it alone. RoundEventBroadcaster now also tracks subscribers per client IP, capped much lower (MAX_SUBSCRIBERS_PER_IP=5). Past that cap, opening one more stream evicts that same IP's own oldest connection (woken via a new EVICTED sentinel so the SSE generator closes it promptly) rather than refusing the new one or letting one abusive IP crowd out unrelated clients under the old global-only cap. The global cap stays as a backstop against overall resource exhaustion regardless of source. Extracted client_ip() (X-Forwarded-For, since Caddy reverse-proxies every request) out of auth/routes.py into app/api/client_ip.py so the login/registration throttles (B-33) and this new per-IP cap share one definition instead of two that could drift apart. Not implemented: enforcing the connection cap at the Caddy layer itself, which the proposed fix also suggested - the standard Caddy image this project uses has no such directive without a third-party module, and building a custom image felt like a bigger, separate change than this fix warranted. Suite grows from 201 to 211 tests. BUGS.md moves B-38 to Previously fixed.
18 lines
742 B
Python
18 lines
742 B
Python
from fastapi import Request
|
|
|
|
|
|
def client_ip(request: Request) -> str:
|
|
"""The caller's real IP, from Caddy's X-Forwarded-For (see Caddyfile) —
|
|
request.client.host would otherwise be the reverse proxy's own address, not
|
|
the caller's. Falls back to request.client.host only if the header is
|
|
somehow missing (e.g. the app container hit directly, bypassing Caddy).
|
|
|
|
Shared by the login/registration throttles (B-33) and the SSE per-IP
|
|
subscriber cap (B-38) so the two can't drift into different notions of
|
|
"the client's IP".
|
|
"""
|
|
forwarded = request.headers.get("x-forwarded-for")
|
|
if forwarded:
|
|
return forwarded.split(",")[0].strip()
|
|
return request.client.host if request.client else "unknown"
|