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
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:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user