From f14451a5de3b8e4283d35ff11190d79292491a7c Mon Sep 17 00:00:00 2001 From: KanteshMurade <97043824+KanteshMurade@users.noreply.github.com> Date: Thu, 28 May 2026 23:56:46 +0530 Subject: [PATCH 1/2] fix(web): add Safari fallback for requestIdleCallback (#9137) * fix(web): add Safari fallback for requestIdleCallback * fix(web): use globalThis in idle-task fallbacks Switch idle-task fallback paths from window.* to globalThis.* so the fallback no longer crashes in environments where window is undefined. Also thread IdleRequestOptions through requestIdleFallback so the caller's timeout hint is honored when falling back. Addresses CodeRabbit review feedback on #9137. --------- Co-authored-by: sriram veeraghanta --- apps/web/app/entry.client.tsx | 4 +++ apps/web/core/lib/idle-task.ts | 47 ++++++++++++++++++++++------ apps/web/core/lib/polyfills/index.ts | 25 ++------------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx index 9c665ede072..8dc2a587717 100644 --- a/apps/web/app/entry.client.tsx +++ b/apps/web/app/entry.client.tsx @@ -8,6 +8,10 @@ import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; +import polyfills from "@/lib/polyfills"; + +void polyfills; + startTransition(() => { hydrateRoot( document, diff --git a/apps/web/core/lib/idle-task.ts b/apps/web/core/lib/idle-task.ts index 34d070bfe40..3d6db72b8f5 100644 --- a/apps/web/core/lib/idle-task.ts +++ b/apps/web/core/lib/idle-task.ts @@ -8,20 +8,47 @@ export type IdleTaskHandle = { cancel: () => void; }; +const requestIdleFallback = (callback: IdleRequestCallback, options?: IdleRequestOptions): number => { + const start = Date.now(); + + return globalThis.setTimeout(() => { + callback({ + didTimeout: false, + timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), + }); + }, options?.timeout ?? 1) as unknown as number; +}; + +const cancelIdleFallback = (id: number) => { + globalThis.clearTimeout(id); +}; + +export const requestIdle = (callback: IdleRequestCallback, options?: IdleRequestOptions): number => { + if (typeof globalThis.requestIdleCallback === "function") return globalThis.requestIdleCallback(callback, options); + + return requestIdleFallback(callback, options); +}; + +export const cancelIdle = (id: number) => { + if (typeof globalThis.cancelIdleCallback === "function") return globalThis.cancelIdleCallback(id); + + return cancelIdleFallback(id); +}; + +export const installIdleCallbackPolyfill = () => { + if (typeof globalThis === "undefined") return; + + globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? requestIdleFallback; + globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? cancelIdleFallback; +}; + /** * Schedule lightweight work for idle time and return a cancel handle. * Falls back to setTimeout when requestIdleCallback is unavailable. */ -export const runIdleTask = (callback: () => void): IdleTaskHandle => { - if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") { - const idleId = window.requestIdleCallback(callback, { timeout: 300 }); - return { - cancel: () => window.cancelIdleCallback(idleId), - }; - } - - const timeoutId = window.setTimeout(callback, 0); +export const runIdleTask = (callback: IdleRequestCallback): IdleTaskHandle => { + const idleId = requestIdle(callback, { timeout: 300 }); return { - cancel: () => window.clearTimeout(timeoutId), + cancel: () => cancelIdle(idleId), }; }; diff --git a/apps/web/core/lib/polyfills/index.ts b/apps/web/core/lib/polyfills/index.ts index 2243dc620c2..fae765ebf8e 100644 --- a/apps/web/core/lib/polyfills/index.ts +++ b/apps/web/core/lib/polyfills/index.ts @@ -4,27 +4,8 @@ * See the LICENSE file for details. */ -if (typeof window !== "undefined" && window) { - // Add request callback polyfill to browser in case it does not exist - window.requestIdleCallback = - window.requestIdleCallback ?? - function (cb) { - const start = Date.now(); - return setTimeout(function () { - cb({ - didTimeout: false, - timeRemaining: function () { - return Math.max(0, 50 - (Date.now() - start)); - }, - }); - }, 1); - }; +import { installIdleCallbackPolyfill } from "@/lib/idle-task"; - window.cancelIdleCallback = - window.cancelIdleCallback ?? - function (id) { - clearTimeout(id); - }; -} +installIdleCallbackPolyfill(); -export {}; +export default true; From 248f5d66e69fa99b64de27aa454819158d19872a Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 29 May 2026 00:13:41 +0530 Subject: [PATCH 2/2] refactor(api): source API_KEY_RATE_LIMIT from settings, drop service token throttle (#9161) - Define API_KEY_RATE_LIMIT in plane/settings/common.py and read it via django.conf.settings in ApiKeyRateThrottle instead of os.environ. - Remove ServiceTokenRateThrottle and the service-token branch in BaseAPIView.get_throttles; all API key requests now go through ApiKeyRateThrottle. --- apps/api/plane/api/rate_limit.py | 47 ++----------------------------- apps/api/plane/api/views/base.py | 17 ++--------- apps/api/plane/settings/common.py | 3 ++ 3 files changed, 8 insertions(+), 59 deletions(-) diff --git a/apps/api/plane/api/rate_limit.py b/apps/api/plane/api/rate_limit.py index 33df895cbfb..7567ec156fc 100644 --- a/apps/api/plane/api/rate_limit.py +++ b/apps/api/plane/api/rate_limit.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. -# python imports -import os +# Django imports +from django.conf import settings # Third party imports from rest_framework.throttling import SimpleRateThrottle @@ -11,48 +11,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle): scope = "api_key" - rate = os.environ.get("API_KEY_RATE_LIMIT", "60/minute") - - def get_cache_key(self, request, view): - # Retrieve the API key from the request header - api_key = request.headers.get("X-Api-Key") - if not api_key: - return None # Allow the request if there's no API key - - # Use the API key as part of the cache key - return f"{self.scope}:{api_key}" - - def allow_request(self, request, view): - allowed = super().allow_request(request, view) - - if allowed: - now = self.timer() - # Calculate the remaining limit and reset time - history = self.cache.get(self.key, []) - - # Remove old histories - while history and history[-1] <= now - self.duration: - history.pop() - - # Calculate the requests - num_requests = len(history) - - # Check available requests - available = self.num_requests - num_requests - - # Unix timestamp for when the rate limit will reset - reset_time = int(now + self.duration) - - # Add headers - request.META["X-RateLimit-Remaining"] = max(0, available) - request.META["X-RateLimit-Reset"] = reset_time - - return allowed - - -class ServiceTokenRateThrottle(SimpleRateThrottle): - scope = "service_token" - rate = "300/minute" + rate = settings.API_KEY_RATE_LIMIT def get_cache_key(self, request, view): # Retrieve the API key from the request header diff --git a/apps/api/plane/api/views/base.py b/apps/api/plane/api/views/base.py index fc65e7abdcf..2fb20c8d178 100644 --- a/apps/api/plane/api/views/base.py +++ b/apps/api/plane/api/views/base.py @@ -22,9 +22,8 @@ from rest_framework.generics import GenericAPIView # Module imports -from plane.db.models.api import APIToken from plane.api.middleware.api_authentication import APIKeyAuthentication -from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle +from plane.api.rate_limit import ApiKeyRateThrottle from plane.utils.exception_logger import log_exception from plane.utils.paginator import BasePaginator from plane.utils.core.mixins import ReadReplicaControlMixin @@ -60,19 +59,7 @@ def filter_queryset(self, queryset): return queryset def get_throttles(self): - throttle_classes = [] - api_key = self.request.headers.get("X-Api-Key") - - if api_key: - service_token = APIToken.objects.filter(token=api_key, is_service=True).first() - - if service_token: - throttle_classes.append(ServiceTokenRateThrottle()) - return throttle_classes - - throttle_classes.append(ApiKeyRateThrottle()) - - return throttle_classes + return [ApiKeyRateThrottle()] def handle_exception(self, exc): """ diff --git a/apps/api/plane/settings/common.py b/apps/api/plane/settings/common.py index 8dba3066e70..65b5d7b9f82 100644 --- a/apps/api/plane/settings/common.py +++ b/apps/api/plane/settings/common.py @@ -132,6 +132,9 @@ "SCHEMA_COERCE_PATH_PK": False, } +# API key throttle rate (DRF SimpleRateThrottle format, e.g. "60/minute") +API_KEY_RATE_LIMIT = os.environ.get("API_KEY_RATE_LIMIT", "60/minute") + # Django Auth Backend AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) # default