backup
Some checks failed
CI / Backend Tests (pull_request) Has been cancelled
CI / Lint (pull_request) Has been cancelled
CI / Type Check (pull_request) Has been cancelled
CI / Import Boundary (pull_request) Has been cancelled
CI / OpenAPI Breaking Changes (pull_request) Has been cancelled
CI / OpenAPI Baseline Commit (pull_request) Has been cancelled

This commit is contained in:
2026-05-20 20:18:58 +02:00
parent 7308ff88d6
commit 83b2cb67b1
2 changed files with 48 additions and 18 deletions

View File

@@ -242,9 +242,9 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# deployments, it should be replaced with a shared backend. # deployments, it should be replaced with a shared backend.
_update_session_cache(app, settings) _update_session_cache(app, settings)
# Initialize the global rate limiter (200 requests per 60 seconds per IP). # Initialize the global rate limiter (600 requests per 60 seconds per IP).
# Applied to all endpoints via middleware. Process-local implementation. # Applied to all endpoints via middleware. Process-local implementation.
app.state.global_rate_limiter = GlobalRateLimiter(max_requests=200, window_seconds=60) app.state.global_rate_limiter = GlobalRateLimiter(max_requests=600, window_seconds=60)
log.info("bangui_started") log.info("bangui_started")
@@ -1095,10 +1095,10 @@ def create_app(settings: Settings | None = None) -> FastAPI:
if resolved_settings.session_cache_enabled and resolved_settings.session_cache_ttl_seconds > 0.0 if resolved_settings.session_cache_enabled and resolved_settings.session_cache_ttl_seconds > 0.0
else NoOpSessionCache() else NoOpSessionCache()
) )
# Initialize the global rate limiter (200 requests per 60 seconds per IP). # Initialize the global rate limiter (600 requests per 60 seconds per IP).
# This is also re-initialized in the lifespan, but must be present here # This is also re-initialized in the lifespan, but must be present here
# for tests that bypass the lifespan via ASGITransport. # for tests that bypass the lifespan via ASGITransport.
app.state.global_rate_limiter = GlobalRateLimiter(max_requests=200, window_seconds=60) app.state.global_rate_limiter = GlobalRateLimiter(max_requests=600, window_seconds=60)
set_setup_complete_cache(app, False) set_setup_complete_cache(app, False)
@@ -1161,13 +1161,25 @@ def create_app(settings: Settings | None = None) -> FastAPI:
path_prefixes=["/api/v1/history"], path_prefixes=["/api/v1/history"],
) )
# Global rate limiter for all other endpoints. # Polling endpoints (blocklist schedule) get a dedicated bucket
# 200 req/min per IP — default protection. # to avoid exhausting the global limit during normal frontend operation.
app.add_middleware( app.add_middleware(
RateLimitMiddleware, RateLimitMiddleware,
rate_limiter=app.state.global_rate_limiter, rate_limiter=app.state.global_rate_limiter,
settings=resolved_settings, settings=resolved_settings,
skip_paths=["/api/v1/auth/login", "/api/v1/setup", "/api/v1/history"], bucket_override="polling:read",
bucket_max_requests=10000,
bucket_window_seconds=60,
path_prefixes=["/api/v1/blocklists/schedule"],
)
# Global rate limiter for all other endpoints.
# 600 req/min per IP — default protection.
app.add_middleware(
RateLimitMiddleware,
rate_limiter=app.state.global_rate_limiter,
settings=resolved_settings,
skip_paths=["/api/v1/auth/login", "/api/v1/setup", "/api/v1/history", "/api/v1/blocklists/schedule"],
) )
# Validate middleware order before returning the app. # Validate middleware order before returning the app.

View File

@@ -77,11 +77,34 @@ export function usePolledData<TResponse, TData>(
pauseWhenHidden = false, pauseWhenHidden = false,
} = options; } = options;
// Stabilize fetcher/selector/onSuccess references so that useFetchData's
// refresh callback (and the useEffect that calls it) don't re-trigger on
// every render when callers pass inline functions.
const fetcherRef = useRef(fetcher);
fetcherRef.current = fetcher;
const selectorRef = useRef(selector);
selectorRef.current = selector;
const onSuccessRef = useRef(onSuccess);
onSuccessRef.current = onSuccess;
const stableFetcher = useCallback(
(signal: AbortSignal) => fetcherRef.current(signal),
[],
);
const stableSelector = useCallback(
(response: TResponse) => selectorRef.current(response),
[],
);
const stableOnSuccess = useCallback(
(response: TResponse) => onSuccessRef.current?.(response),
[],
);
const { data, loading, error, refresh } = useFetchData({ const { data, loading, error, refresh } = useFetchData({
fetcher, fetcher: stableFetcher,
selector, selector: stableSelector,
errorMessage, errorMessage,
onSuccess, onSuccess: onSuccessRef.current ? stableOnSuccess : undefined,
initialData, initialData,
}); });
@@ -151,15 +174,10 @@ export function usePolledData<TResponse, TData>(
return; return;
} }
// Record when polling starts and schedule first poll immediately // Record when polling starts. The initial fetch is handled by useFetchData's
// mount effect, so we just mark the start time and let the loading-completion
// effect (above) schedule the first poll after the initial fetch finishes.
pollStartTimeRef.current = performance.now(); pollStartTimeRef.current = performance.now();
const id = window.setTimeout((): void => {
if (cancelledRef.current) return;
pollStartTimeRef.current = performance.now();
refreshRef.current?.();
}, 0);
timeoutIdRef.current = id;
return (): void => { return (): void => {
cancelledRef.current = true; cancelledRef.current = true;