-
Notifications
You must be signed in to change notification settings - Fork 1
fix(gateway): fail fast + cache tool lookup on WS upgrade (+ upstream-failure logging) #1255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
eb8c45f
1dfd722
4a293bc
0b45e5d
8d30f54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| package com.openframe.gateway.config.ws; | ||
|
|
||
| import com.github.benmanes.caffeine.cache.Cache; | ||
| import com.github.benmanes.caffeine.cache.Caffeine; | ||
| import com.openframe.data.document.tool.IntegratedTool; | ||
| import com.openframe.data.reactive.repository.tool.ReactiveIntegratedToolRepository; | ||
| import com.openframe.data.service.TenantIdProvider; | ||
|
|
@@ -15,22 +17,34 @@ | |
| import org.springframework.core.Ordered; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.HttpStatusCode; | ||
| import org.springframework.http.server.reactive.ServerHttpRequest; | ||
| import org.springframework.http.server.reactive.ServerHttpResponse; | ||
| import org.springframework.web.server.ResponseStatusException; | ||
| import org.springframework.web.server.ServerWebExchange; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| import java.net.URI; | ||
| import java.time.Duration; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public abstract class ToolWebSocketProxyUrlFilter implements GatewayFilter, Ordered { | ||
|
|
||
| public static final String ORIGINAL_AUTHORIZATION_ATTR = "originalAuthorization"; | ||
|
|
||
| // Bounds the per-upgrade tool lookup so a stalled Mongo query fails fast instead of letting the WS upgrade hang to the gateway response-timeout. | ||
| private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(5); | ||
|
|
||
| private final ReactiveIntegratedToolRepository toolRepository; | ||
| private final ToolUpstreamResolverRegistry upstreamRegistry; | ||
|
|
||
| // Cache resolved tools briefly so an agent reconnect storm doesn't put a Mongo lookup on every WS upgrade (configs change rarely). | ||
| private final Cache<String, IntegratedTool> toolCache = Caffeine.newBuilder() | ||
| .maximumSize(10_000) | ||
| .expireAfterWrite(Duration.ofSeconds(60)) | ||
| .build(); | ||
|
|
||
| /** | ||
| * Shared multi-tenant routing mode. When true, a tool lookup with no resolved tenant must NOT | ||
| * fall back to an unscoped query. Defaults false so single-tenant / OSS pods keep prior behavior. | ||
|
|
@@ -68,7 +82,17 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { | |
|
|
||
| ServerWebExchange mutatedExchange = mutateExchange(exchange, tool); | ||
| return chain.filter(mutatedExchange); | ||
| }); | ||
| }) | ||
| // Without this, a lookup error/timeout left the WS upgrade hanging with no response written, so the agent saw "No HTTP response" and retried forever; fail fast with a real status instead. | ||
| .onErrorResume(ex -> abortUpgrade(exchange, toolId, ex)); | ||
| } | ||
|
|
||
| private Mono<Void> abortUpgrade(ServerWebExchange exchange, String toolId, Throwable ex) { | ||
| HttpStatusCode status = (ex instanceof ResponseStatusException rse) ? rse.getStatusCode() : HttpStatus.SERVICE_UNAVAILABLE; | ||
| log.warn("Tool websocket upgrade aborted (tool={}, status={}): {}", toolId, status, ex.toString()); | ||
| ServerHttpResponse response = exchange.getResponse(); | ||
| response.setStatusCode(status); | ||
| return response.setComplete(); | ||
|
Comment on lines
+85
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major 🧩 Analysis chain🌐 Web query:
💡 Result: When you call setStatusCode(HttpStatusCode) on a Spring WebFlux ServerHttpResponse, the method returns a boolean value indicating whether the status code was successfully applied [1]. The return value behavior is as follows: - Returns true if the status code was successfully set [1]. - Returns false if the status code change could not be processed because the HTTP response is already committed [1]. A response is considered "committed" once the headers and the initial part of the response body have been sent to the client [2][3]. Once the response is in this state, it is no longer possible to modify the status code or headers, as they have already been written to the underlying network stream [1][4]. In practice, developers often use the boolean return value to conditionally handle errors or log status changes, ensuring that they do not attempt to set a status code when it is too late to do so [5][6]. Because attempts to change the status after commitment are ignored (resulting in a false return value), application logic should be designed to handle potential failures or status changes before any part of the response body is emitted [3]. If you need to perform actions or set statuses precisely before the response is committed, you can use the beforeCommit(Supplier) hook provided by the ServerHttpResponse [1][4]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the target file and inspect the surrounding implementation.
git ls-files 'openframe-gateway-service-core/src/main/java/com/openframe/gateway/config/ws/ToolWebSocketProxyUrlFilter.java'
wc -l openframe-gateway-service-core/src/main/java/com/openframe/gateway/config/ws/ToolWebSocketProxyUrlFilter.java
sed -n '1,220p' openframe-gateway-service-core/src/main/java/com/openframe/gateway/config/ws/ToolWebSocketProxyUrlFilter.javaRepository: flamingo-stack/openframe-oss-lib Length of output: 7260 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the filter implementation and related WebSocket upgrade flow.
sed -n '1,220p' openframe-gateway-service-core/src/main/java/com/openframe/gateway/config/ws/ToolWebSocketProxyUrlFilter.java
printf '\n--- SEARCH ---\n'
rg -n "chain\.filter|onErrorResume|setStatusCode|isCommitted|upgrade|WebSocket" openframe-gateway-service-core/src/main/java/com/openframe/gateway/config/ws -g '*.java'Repository: flamingo-stack/openframe-oss-lib Length of output: 19048 Skip 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| protected ServerWebExchange mutateExchange(ServerWebExchange exchange, IntegratedTool tool) { | ||
|
|
@@ -82,6 +106,14 @@ protected ServerWebExchange mutateExchange(ServerWebExchange exchange, Integrate | |
| * headers are never read; the unscoped {@code findByKey} is correct (one tenant only). | ||
| */ | ||
| private Mono<IntegratedTool> getTool(String toolId, ServerHttpRequest request) { | ||
| String cacheKey = tenantRoutingEnabled | ||
| ? (TenantRoutingHeaders.tenantId(request) + "::" + toolId) | ||
| : toolId; | ||
| IntegratedTool cached = toolCache.getIfPresent(cacheKey); | ||
| if (cached != null) { | ||
| return Mono.just(cached); | ||
| } | ||
|
|
||
| Mono<IntegratedTool> lookup = tenantRoutingEnabled | ||
| ? toolRepository.findByTenantIdAndKey(TenantRoutingHeaders.tenantId(request), toolId) | ||
| : toolRepository.findByKey(toolId); | ||
|
|
@@ -94,7 +126,10 @@ private Mono<IntegratedTool> getTool(String toolId, ServerHttpRequest request) { | |
| return Mono.error(new IllegalArgumentException("Tool " + tool.getName() + " is not enabled")); | ||
| } | ||
| return Mono.just(tool); | ||
| }); | ||
| }) | ||
| .timeout(LOOKUP_TIMEOUT) | ||
| // Only successful (present + enabled) tools are cached; not-found / not-enabled stay uncached so they recover immediately once fixed. | ||
| .doOnNext(tool -> toolCache.put(cacheKey, tool)); | ||
| } | ||
|
|
||
| protected abstract String getRequestToolId(String path); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Avoid caching revocation state without invalidation.
Once an enabled tool is cached, later upgrades bypass the repository and the
isEnabled()check for up to 60 seconds. If disabling/deleting a tool is an access-revocation action, add cache invalidation on tool updates or reduce/remove caching for that gate.Also applies to: 112-132
🤖 Prompt for AI Agents