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.
This commit is contained in:
Anthony Stirling
2026-02-18 10:52:59 +00:00
committed by GitHub
parent ddf93d2b1a
commit ae9d29abf0
5 changed files with 37 additions and 57 deletions
@@ -606,6 +606,12 @@ public class EndpointConfiguration {
return endpointGroups.getOrDefault(group, new HashSet<>()); return endpointGroups.getOrDefault(group, new HashSet<>());
} }
public Set<String> getAllEndpoints() {
return endpointGroups.values().stream()
.flatMap(Set::stream)
.collect(java.util.stream.Collectors.toSet());
}
private boolean isToolGroup(String group) { private boolean isToolGroup(String group) {
return "qpdf".equals(group) return "qpdf".equals(group)
|| "OCRmyPDF".equals(group) || "OCRmyPDF".equals(group)
@@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -11,9 +12,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.config.EndpointConfiguration;
@@ -320,11 +318,13 @@ public class ConfigController {
@GetMapping("/endpoints-availability") @GetMapping("/endpoints-availability")
public ResponseEntity<Map<String, EndpointAvailability>> getEndpointAvailability( public ResponseEntity<Map<String, EndpointAvailability>> getEndpointAvailability(
@RequestParam(name = "endpoints") @RequestParam(name = "endpoints", required = false) List<String> endpoints) {
@Size(min = 1, max = 100, message = "Must provide between 1 and 100 endpoints") Collection<String> toCheck =
List<@NotBlank String> endpoints) { (endpoints == null || endpoints.isEmpty())
? endpointConfiguration.getAllEndpoints()
: endpoints;
Map<String, EndpointAvailability> result = new HashMap<>(); Map<String, EndpointAvailability> result = new HashMap<>();
for (String endpoint : endpoints) { for (String endpoint : toCheck) {
String trimmedEndpoint = endpoint.trim(); String trimmedEndpoint = endpoint.trim();
result.put( result.put(
trimmedEndpoint, trimmedEndpoint,
@@ -38,6 +38,7 @@ spring.devtools.livereload.enabled=true
spring.devtools.restart.exclude=stirling.software.proprietary.security/** spring.devtools.restart.exclude=stirling.software.proprietary.security/**
spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.web.resources.mime-mappings.webmanifest=application/manifest+json
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} 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.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 spring.datasource.driver-class-name=org.h2.Driver
+22 -47
View File
@@ -2,8 +2,8 @@ import { useState, useEffect } from 'react';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability'; import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability';
// Track globally fetched endpoint sets to prevent duplicate fetches across components // Track whether we've done the global fetch to prevent duplicate requests
const globalFetchedSets = new Set<string>(); let globalFetchDone = false;
const globalEndpointCache: Record<string, EndpointAvailabilityDetails> = {}; const globalEndpointCache: Record<string, EndpointAvailabilityDetails> = {};
/** /**
@@ -72,17 +72,17 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchAllEndpointStatuses = async (force = false) => { const fetchAllEndpointStatuses = async (force = false) => {
const endpointsKey = [...endpoints].sort().join(','); // Skip if already fetched globally and not forced
if (!force && globalFetchDone) {
// Skip if we already fetched these exact endpoints globally console.debug('[useEndpointConfig] Using global cache');
if (!force && globalFetchedSets.has(endpointsKey)) {
console.debug('[useEndpointConfig] Already fetched these endpoints globally, using cache');
const cached = endpoints.reduce( const cached = endpoints.reduce(
(acc, endpoint) => { (acc, endpoint) => {
const cachedDetails = globalEndpointCache[endpoint]; const cachedDetails = globalEndpointCache[endpoint];
if (cachedDetails) { if (cachedDetails) {
acc.status[endpoint] = cachedDetails.enabled; acc.status[endpoint] = cachedDetails.enabled;
acc.details[endpoint] = cachedDetails; acc.details[endpoint] = cachedDetails;
} else {
acc.status[endpoint] = true;
} }
return acc; return acc;
}, },
@@ -93,6 +93,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
setLoading(false); setLoading(false);
return; return;
} }
if (!endpoints || endpoints.length === 0) { if (!endpoints || endpoints.length === 0) {
setEndpointStatus({}); setEndpointStatus({});
setEndpointDetails({}); setEndpointDetails({});
@@ -103,45 +104,21 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
try { try {
setLoading(true); setLoading(true);
setError(null); 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 // Fetch all endpoints at once - no query params needed
const newEndpoints = endpoints.filter(ep => !(ep in globalEndpointCache)); const response = await apiClient.get<Record<string, EndpointAvailabilityDetails>>(`/api/v1/config/endpoints-availability`);
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<string, boolean>, details: {} as Record<string, EndpointAvailabilityDetails> }
);
setEndpointStatus(cached.status);
setEndpointDetails(prev => ({ ...prev, ...cached.details }));
globalFetchedSets.add(endpointsKey);
setLoading(false);
return;
}
// Use batch API for efficiency - only fetch new endpoints // Populate global cache with all results
const endpointsParam = newEndpoints.join(','); Object.entries(response.data).forEach(([endpoint, details]) => {
const response = await apiClient.get<Record<string, EndpointAvailabilityDetails>>(`/api/v1/config/endpoints-availability?endpoints=${encodeURIComponent(endpointsParam)}`);
const statusMap = response.data;
// Update global cache with new results
Object.entries(statusMap).forEach(([endpoint, details]) => {
globalEndpointCache[endpoint] = { globalEndpointCache[endpoint] = {
enabled: details?.enabled ?? true, enabled: details?.enabled ?? true,
reason: details?.reason ?? null, 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( const fullStatus = endpoints.reduce(
(acc, endpoint) => { (acc, endpoint) => {
const cachedDetails = globalEndpointCache[endpoint]; const cachedDetails = globalEndpointCache[endpoint];
@@ -158,17 +135,17 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
setEndpointStatus(fullStatus.status); setEndpointStatus(fullStatus.status);
setEndpointDetails(prev => ({ ...prev, ...fullStatus.details })); setEndpointDetails(prev => ({ ...prev, ...fullStatus.details }));
globalFetchedSets.add(endpointsKey);
} catch (err: any) { } catch (err: any) {
// On 401 (auth error), use optimistic fallback instead of disabling // On 401 (auth error), use optimistic fallback instead of disabling
if (err.response?.status === 401) { if (err.response?.status === 401) {
console.warn('[useEndpointConfig] 401 error - using optimistic fallback'); console.warn('[useEndpointConfig] 401 error - using optimistic fallback');
endpoints.forEach(endpoint => {
globalEndpointCache[endpoint] = { enabled: true, reason: null };
});
const optimisticStatus = endpoints.reduce( const optimisticStatus = endpoints.reduce(
(acc, endpoint) => { (acc, endpoint) => {
const optimisticDetails: EndpointAvailabilityDetails = { enabled: true, reason: null };
acc.status[endpoint] = true; acc.status[endpoint] = true;
acc.details[endpoint] = optimisticDetails; acc.details[endpoint] = { enabled: true, reason: null };
globalEndpointCache[endpoint] = optimisticDetails;
return acc; return acc;
}, },
{ status: {} as Record<string, boolean>, details: {} as Record<string, EndpointAvailabilityDetails> } { status: {} as Record<string, boolean>, details: {} as Record<string, EndpointAvailabilityDetails> }
@@ -181,14 +158,13 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
setError(errorMessage); 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) // Fallback: assume all endpoints are enabled on error (optimistic)
const optimisticStatus = endpoints.reduce( const optimisticStatus = endpoints.reduce(
(acc, endpoint) => { (acc, endpoint) => {
const optimisticDetails: EndpointAvailabilityDetails = { enabled: true, reason: null };
acc.status[endpoint] = true; acc.status[endpoint] = true;
acc.details[endpoint] = optimisticDetails; acc.details[endpoint] = { enabled: true, reason: null };
return acc; return acc;
}, },
{ status: {} as Record<string, boolean>, details: {} as Record<string, EndpointAvailabilityDetails> } { status: {} as Record<string, boolean>, details: {} as Record<string, EndpointAvailabilityDetails> }
@@ -208,8 +184,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
useEffect(() => { useEffect(() => {
const handleJwtAvailable = () => { const handleJwtAvailable = () => {
console.debug('[useEndpointConfig] JWT available event - clearing cache for refetch with auth'); console.debug('[useEndpointConfig] JWT available event - clearing cache for refetch with auth');
// Clear the global cache to allow refetch with JWT globalFetchDone = false;
globalFetchedSets.clear();
Object.keys(globalEndpointCache).forEach(key => delete globalEndpointCache[key]); Object.keys(globalEndpointCache).forEach(key => delete globalEndpointCache[key]);
fetchAllEndpointStatuses(true); fetchAllEndpointStatuses(true);
}; };
@@ -190,10 +190,8 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
try { try {
setError(null); setError(null);
const endpointsParam = endpoints.join(',');
const response = await apiClient.get<Record<string, EndpointAvailabilityDetails>>( const response = await apiClient.get<Record<string, EndpointAvailabilityDetails>>(
`/api/v1/config/endpoints-availability?endpoints=${encodeURIComponent(endpointsParam)}`, `/api/v1/config/endpoints-availability`,
{ {
suppressErrorToast: true, suppressErrorToast: true,
} }