From ae9d29abf0722e89bb16c0139eb85c6ab630117f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:52:59 +0000 Subject: [PATCH] large query reduction (#5754) # Description of Changes Reduce endpoint-availability call so that an empty param to it returns all endpoints to avoid pointlessly large http headers Before: GET /api/v1/config/endpoints-availability?endpoints=compress-pdf%2Crotate-pdf%2Cmerge-pdfs%2Csplit-pages%2Cocr-pdf for all 74 tools After: GET /api/v1/config/endpoints-availability --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../SPDF/config/EndpointConfiguration.java | 6 ++ .../controller/api/misc/ConfigController.java | 14 ++-- .../src/main/resources/application.properties | 1 + frontend/src/core/hooks/useEndpointConfig.ts | 69 ++++++------------- .../src/desktop/hooks/useEndpointConfig.ts | 4 +- 5 files changed, 37 insertions(+), 57 deletions(-) diff --git a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 4a4d9cf8d..acb222ea5 100644 --- a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -606,6 +606,12 @@ public class EndpointConfiguration { return endpointGroups.getOrDefault(group, new HashSet<>()); } + public Set getAllEndpoints() { + return endpointGroups.values().stream() + .flatMap(Set::stream) + .collect(java.util.stream.Collectors.toSet()); + } + private boolean isToolGroup(String group) { return "qpdf".equals(group) || "OCRmyPDF".equals(group) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index 247e8e18b..dabaab32f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.controller.api.misc; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -11,9 +12,6 @@ import org.springframework.web.bind.annotation.RequestParam; import io.swagger.v3.oas.annotations.Hidden; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.EndpointConfiguration; @@ -320,11 +318,13 @@ public class ConfigController { @GetMapping("/endpoints-availability") public ResponseEntity> getEndpointAvailability( - @RequestParam(name = "endpoints") - @Size(min = 1, max = 100, message = "Must provide between 1 and 100 endpoints") - List<@NotBlank String> endpoints) { + @RequestParam(name = "endpoints", required = false) List endpoints) { + Collection toCheck = + (endpoints == null || endpoints.isEmpty()) + ? endpointConfiguration.getAllEndpoints() + : endpoints; Map result = new HashMap<>(); - for (String endpoint : endpoints) { + for (String endpoint : toCheck) { String trimmedEndpoint = endpoint.trim(); result.put( trimmedEndpoint, diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index c35a4d273..f3ef8d01f 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -38,6 +38,7 @@ spring.devtools.livereload.enabled=true spring.devtools.restart.exclude=stirling.software.proprietary.security/** spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} +server.tomcat.max-http-header-size=32768 spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL spring.datasource.driver-class-name=org.h2.Driver diff --git a/frontend/src/core/hooks/useEndpointConfig.ts b/frontend/src/core/hooks/useEndpointConfig.ts index 1a1db7f8b..96b3f2f12 100644 --- a/frontend/src/core/hooks/useEndpointConfig.ts +++ b/frontend/src/core/hooks/useEndpointConfig.ts @@ -2,8 +2,8 @@ import { useState, useEffect } from 'react'; import apiClient from '@app/services/apiClient'; import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability'; -// Track globally fetched endpoint sets to prevent duplicate fetches across components -const globalFetchedSets = new Set(); +// Track whether we've done the global fetch to prevent duplicate requests +let globalFetchDone = false; const globalEndpointCache: Record = {}; /** @@ -72,17 +72,17 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { const [error, setError] = useState(null); const fetchAllEndpointStatuses = async (force = false) => { - const endpointsKey = [...endpoints].sort().join(','); - - // Skip if we already fetched these exact endpoints globally - if (!force && globalFetchedSets.has(endpointsKey)) { - console.debug('[useEndpointConfig] Already fetched these endpoints globally, using cache'); + // Skip if already fetched globally and not forced + if (!force && globalFetchDone) { + console.debug('[useEndpointConfig] Using global cache'); const cached = endpoints.reduce( (acc, endpoint) => { const cachedDetails = globalEndpointCache[endpoint]; if (cachedDetails) { acc.status[endpoint] = cachedDetails.enabled; acc.details[endpoint] = cachedDetails; + } else { + acc.status[endpoint] = true; } return acc; }, @@ -93,6 +93,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { setLoading(false); return; } + if (!endpoints || endpoints.length === 0) { setEndpointStatus({}); setEndpointDetails({}); @@ -103,45 +104,21 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { try { setLoading(true); setError(null); - console.debug('[useEndpointConfig] Fetching endpoint statuses', { count: endpoints.length, force }); + console.debug('[useEndpointConfig] Fetching all endpoint statuses from server'); - // Check which endpoints we haven't fetched yet - const newEndpoints = endpoints.filter(ep => !(ep in globalEndpointCache)); - if (newEndpoints.length === 0) { - console.debug('[useEndpointConfig] All endpoints already in global cache'); - const cached = endpoints.reduce( - (acc, endpoint) => { - const cachedDetails = globalEndpointCache[endpoint]; - if (cachedDetails) { - acc.status[endpoint] = cachedDetails.enabled; - acc.details[endpoint] = cachedDetails; - } - return acc; - }, - { status: {} as Record, details: {} as Record } - ); - setEndpointStatus(cached.status); - setEndpointDetails(prev => ({ ...prev, ...cached.details })); - globalFetchedSets.add(endpointsKey); - setLoading(false); - return; - } + // Fetch all endpoints at once - no query params needed + const response = await apiClient.get>(`/api/v1/config/endpoints-availability`); - // Use batch API for efficiency - only fetch new endpoints - const endpointsParam = newEndpoints.join(','); - - const response = await apiClient.get>(`/api/v1/config/endpoints-availability?endpoints=${encodeURIComponent(endpointsParam)}`); - const statusMap = response.data; - - // Update global cache with new results - Object.entries(statusMap).forEach(([endpoint, details]) => { + // Populate global cache with all results + Object.entries(response.data).forEach(([endpoint, details]) => { globalEndpointCache[endpoint] = { enabled: details?.enabled ?? true, reason: details?.reason ?? null, }; }); + globalFetchDone = true; - // Get all requested endpoints from cache (including previously cached ones) + // Return status for the requested endpoints const fullStatus = endpoints.reduce( (acc, endpoint) => { const cachedDetails = globalEndpointCache[endpoint]; @@ -158,17 +135,17 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { setEndpointStatus(fullStatus.status); setEndpointDetails(prev => ({ ...prev, ...fullStatus.details })); - globalFetchedSets.add(endpointsKey); } catch (err: any) { // On 401 (auth error), use optimistic fallback instead of disabling if (err.response?.status === 401) { console.warn('[useEndpointConfig] 401 error - using optimistic fallback'); + endpoints.forEach(endpoint => { + globalEndpointCache[endpoint] = { enabled: true, reason: null }; + }); const optimisticStatus = endpoints.reduce( (acc, endpoint) => { - const optimisticDetails: EndpointAvailabilityDetails = { enabled: true, reason: null }; acc.status[endpoint] = true; - acc.details[endpoint] = optimisticDetails; - globalEndpointCache[endpoint] = optimisticDetails; + acc.details[endpoint] = { enabled: true, reason: null }; return acc; }, { status: {} as Record, details: {} as Record } @@ -181,14 +158,13 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; setError(errorMessage); - console.error('[EndpointConfig] Failed to check multiple endpoints:', err); + console.error('[EndpointConfig] Failed to check endpoints:', err); // Fallback: assume all endpoints are enabled on error (optimistic) const optimisticStatus = endpoints.reduce( (acc, endpoint) => { - const optimisticDetails: EndpointAvailabilityDetails = { enabled: true, reason: null }; acc.status[endpoint] = true; - acc.details[endpoint] = optimisticDetails; + acc.details[endpoint] = { enabled: true, reason: null }; return acc; }, { status: {} as Record, details: {} as Record } @@ -208,8 +184,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { useEffect(() => { const handleJwtAvailable = () => { console.debug('[useEndpointConfig] JWT available event - clearing cache for refetch with auth'); - // Clear the global cache to allow refetch with JWT - globalFetchedSets.clear(); + globalFetchDone = false; Object.keys(globalEndpointCache).forEach(key => delete globalEndpointCache[key]); fetchAllEndpointStatuses(true); }; diff --git a/frontend/src/desktop/hooks/useEndpointConfig.ts b/frontend/src/desktop/hooks/useEndpointConfig.ts index b59f2492e..0ee870b81 100644 --- a/frontend/src/desktop/hooks/useEndpointConfig.ts +++ b/frontend/src/desktop/hooks/useEndpointConfig.ts @@ -190,10 +190,8 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { try { setError(null); - const endpointsParam = endpoints.join(','); - const response = await apiClient.get>( - `/api/v1/config/endpoints-availability?endpoints=${encodeURIComponent(endpointsParam)}`, + `/api/v1/config/endpoints-availability`, { suppressErrorToast: true, }