From 070643ec0c88e59c37b85043d5769e01e9907275 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Tue, 23 Jun 2026 14:29:45 +0530 Subject: [PATCH 1/8] Adding changes for HCM level timeout configurations --- gateway/configs/config-template.toml | 12 ++ .../gateway-controller/pkg/config/config.go | 39 +++++- .../pkg/config/config_test.go | 99 +++++++++++++++ .../pkg/constants/constants.go | 4 + .../gateway-controller/pkg/xds/translator.go | 7 ++ .../pkg/xds/translator_test.go | 50 ++++++++ ...ckend_timeout.feature => timeouts.feature} | 49 +++++++- gateway/it/steps_backend_timeout.go | 60 --------- gateway/it/steps_timeouts.go | 115 ++++++++++++++++++ gateway/it/suite_test.go | 2 +- gateway/it/test-config.toml | 4 + 11 files changed, 374 insertions(+), 67 deletions(-) rename gateway/it/features/{backend_timeout.feature => timeouts.feature} (65%) delete mode 100644 gateway/it/steps_backend_timeout.go create mode 100644 gateway/it/steps_timeouts.go diff --git a/gateway/configs/config-template.toml b/gateway/configs/config-template.toml index d11d5ebb66..94aa4be649 100644 --- a/gateway/configs/config-template.toml +++ b/gateway/configs/config-template.toml @@ -212,6 +212,18 @@ connect_timeout_ms = 5000 server_header_transformation = "OVERWRITE" server_header_value = "WSO2 API Platform" +# HTTP Connection Manager (downstream) timeouts +# Except for idle_timeout, a value of 0 disables all other timeout settings. +[router.http_listener.timeouts] +# Max duration for the entire downstream request +request_timeout = "0s" +# Max duration to receive the complete request headers +request_headers_timeout = "0s" +# Idle timeout for a single HTTP stream/request +stream_idle_timeout = "5m" +# Idle timeout for the downstream connection +idle_timeout = "1h" + [router.policy_engine] host = "policy-engine" port = 9001 diff --git a/gateway/gateway-controller/pkg/config/config.go b/gateway/gateway-controller/pkg/config/config.go index 6402427984..83f9060974 100644 --- a/gateway/gateway-controller/pkg/config/config.go +++ b/gateway/gateway-controller/pkg/config/config.go @@ -520,8 +520,17 @@ type VHostEntry struct { // HTTPListenerConfig holds HTTP listener related configuration of an API type HTTPListenerConfig struct { - ServerHeaderTransformation string `koanf:"server_header_transformation"` // Options: "APPEND_IF_ABSENT", "OVERWRITE", "PASS_THROUGH" - ServerHeaderValue string `koanf:"server_header_value"` // Custom value for the Server header + ServerHeaderTransformation string `koanf:"server_header_transformation"` // Options: "APPEND_IF_ABSENT", "OVERWRITE", "PASS_THROUGH" + ServerHeaderValue string `koanf:"server_header_value"` // Custom value for the Server header + Timeouts HCMTimeouts `koanf:"timeouts"` // HTTP Connection Manager (downstream) timeouts +} + +// HCMTimeouts holds HTTP Connection Manager (downstream/connection) timeouts. +type HCMTimeouts struct { + RequestTimeout time.Duration `koanf:"request_timeout"` // HCM request_timeout (default 0s = disabled) + RequestHeadersTimeout time.Duration `koanf:"request_headers_timeout"` // HCM request_headers_timeout (default 0s = disabled) + StreamIdleTimeout time.Duration `koanf:"stream_idle_timeout"` // HCM stream_idle_timeout (default 5m) + IdleTimeout time.Duration `koanf:"idle_timeout"` // common_http_protocol_options.idle_timeout (default 1h) } // PolicyEngineConfig holds policy engine ext_proc filter configuration @@ -893,6 +902,12 @@ func defaultConfig() *Config { HTTPListener: HTTPListenerConfig{ ServerHeaderTransformation: commonconstants.OVERWRITE, ServerHeaderValue: commonconstants.ServerName, + Timeouts: HCMTimeouts{ + RequestTimeout: 0, // 0s = disabled (Envoy default) + RequestHeadersTimeout: 0, // 0s = disabled (Envoy default) + StreamIdleTimeout: 5 * time.Minute, // Envoy default + IdleTimeout: 1 * time.Hour, // Envoy default (connection-level) + }, }, }, Analytics: AnalyticsConfig{ @@ -1592,6 +1607,26 @@ func (c *Config) validateTimeoutConfig() error { timeouts.ConnectTimeoutMs, constants.MaxReasonableTimeoutMs) } + // Validate HCM (downstream/connection) timeouts. Unlike the upstream timeouts above, + // 0 is a valid value here: which denotes "disabled scenario" + // only reject negative values and unreasonably large ones are rejected. + maxConnTimeout := time.Duration(constants.MaxReasonableConnectionTimeoutMs) * time.Millisecond + hcmTimeouts := map[string]time.Duration{ + "request_timeout": c.Router.HTTPListener.Timeouts.RequestTimeout, + "request_headers_timeout": c.Router.HTTPListener.Timeouts.RequestHeadersTimeout, + "stream_idle_timeout": c.Router.HTTPListener.Timeouts.StreamIdleTimeout, + "idle_timeout": c.Router.HTTPListener.Timeouts.IdleTimeout, + } + for name, v := range hcmTimeouts { + if v < 0 { + return fmt.Errorf("router.http_listener.timeouts.%s must not be negative, got: %s", name, v) + } + if v > maxConnTimeout { + return fmt.Errorf("router.http_listener.timeouts.%s (%s) exceeds maximum reasonable timeout of %s", + name, v, maxConnTimeout) + } + } + return nil } diff --git a/gateway/gateway-controller/pkg/config/config_test.go b/gateway/gateway-controller/pkg/config/config_test.go index 91e53733a4..5d6538a15d 100644 --- a/gateway/gateway-controller/pkg/config/config_test.go +++ b/gateway/gateway-controller/pkg/config/config_test.go @@ -1004,6 +1004,61 @@ func TestConfig_ValidateTimeoutConfig(t *testing.T) { } } +func TestConfig_ValidateHCMTimeouts(t *testing.T) { + maxConn := time.Duration(constants.MaxReasonableConnectionTimeoutMs) * time.Millisecond + tests := []struct { + name string + timeouts HCMTimeouts + wantErr bool + errContains string + }{ + { + name: "Envoy defaults are valid", + timeouts: HCMTimeouts{RequestTimeout: 0, RequestHeadersTimeout: 0, StreamIdleTimeout: 5 * time.Minute, IdleTimeout: time.Hour}, + }, + { + name: "All zero (disabled) is valid", + timeouts: HCMTimeouts{}, + }, + { + name: "Custom positive values are valid", + timeouts: HCMTimeouts{RequestTimeout: 30 * time.Second, RequestHeadersTimeout: 10 * time.Second, StreamIdleTimeout: time.Minute, IdleTimeout: 2 * time.Hour}, + }, + { + name: "Negative request_timeout rejected", + timeouts: HCMTimeouts{RequestTimeout: -1 * time.Second}, + wantErr: true, + errContains: "router.http_listener.timeouts.request_timeout must not be negative", + }, + { + name: "Negative stream_idle_timeout rejected", + timeouts: HCMTimeouts{StreamIdleTimeout: -5 * time.Second}, + wantErr: true, + errContains: "router.http_listener.timeouts.stream_idle_timeout must not be negative", + }, + { + name: "idle_timeout exceeding max reasonable rejected", + timeouts: HCMTimeouts{IdleTimeout: maxConn + time.Second}, + wantErr: true, + errContains: "router.http_listener.timeouts.idle_timeout", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := validConfig() + cfg.Router.HTTPListener.Timeouts = tt.timeouts + err := cfg.Validate() + if tt.wantErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errContains) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestConfig_ValidatePolicyEngineConfig(t *testing.T) { tests := []struct { name string @@ -1582,6 +1637,50 @@ func TestDefaultConfig(t *testing.T) { assert.Equal(t, "info", cfg.Controller.Logging.Level) assert.False(t, cfg.Controller.Server.SkipInvalidDeploymentsOnStartup) assert.Equal(t, uint32(5000), cfg.Router.Upstream.Timeouts.ConnectTimeoutMs, "default router.upstream.timeouts.connect_timeout_ms should be 5s (5000 ms)") + + // HCM timeout defaults must match Envoy's documented defaults. + hcm := cfg.Router.HTTPListener.Timeouts + assert.Equal(t, time.Duration(0), hcm.RequestTimeout, "default request_timeout should be 0s (disabled)") + assert.Equal(t, time.Duration(0), hcm.RequestHeadersTimeout, "default request_headers_timeout should be 0s (disabled)") + assert.Equal(t, 5*time.Minute, hcm.StreamIdleTimeout, "default stream_idle_timeout should be 5m") + assert.Equal(t, time.Hour, hcm.IdleTimeout, "default idle_timeout should be 1h") +} + +func TestLoadConfig_HCMTimeouts(t *testing.T) { + t.Run("explicit values parse from toml", func(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.toml") + toml := ` +[router.http_listener.timeouts] +request_timeout = "30s" +request_headers_timeout = "10s" +stream_idle_timeout = "2m" +idle_timeout = "30m" +` + require.NoError(t, os.WriteFile(configPath, []byte(toml), 0o644)) + + cfg, err := LoadConfig(configPath) + require.NoError(t, err) + hcm := cfg.Router.HTTPListener.Timeouts + assert.Equal(t, 30*time.Second, hcm.RequestTimeout) + assert.Equal(t, 10*time.Second, hcm.RequestHeadersTimeout) + assert.Equal(t, 2*time.Minute, hcm.StreamIdleTimeout) + assert.Equal(t, 30*time.Minute, hcm.IdleTimeout) + }) + + t.Run("omitted section falls back to Envoy defaults", func(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.toml") + require.NoError(t, os.WriteFile(configPath, []byte(""), 0o644)) + + cfg, err := LoadConfig(configPath) + require.NoError(t, err) + hcm := cfg.Router.HTTPListener.Timeouts + assert.Equal(t, time.Duration(0), hcm.RequestTimeout) + assert.Equal(t, time.Duration(0), hcm.RequestHeadersTimeout) + assert.Equal(t, 5*time.Minute, hcm.StreamIdleTimeout) + assert.Equal(t, time.Hour, hcm.IdleTimeout) + }) } func TestConfig_CaseInsensitiveAlgorithm(t *testing.T) { diff --git a/gateway/gateway-controller/pkg/constants/constants.go b/gateway/gateway-controller/pkg/constants/constants.go index b0741044af..cced5e512a 100644 --- a/gateway/gateway-controller/pkg/constants/constants.go +++ b/gateway/gateway-controller/pkg/constants/constants.go @@ -71,6 +71,10 @@ const ( // Configuration Validation Constants MaxReasonableTimeoutMs = uint32(3600000) // 1 hour in milliseconds MaxReasonablePolicyTimeoutMs = uint32(60000) // 60 seconds in milliseconds + + // MaxReasonableConnectionTimeoutMs caps connection-level timeouts (request, request-headers,etc.), + // allowing higher values than MaxReasonableTimeoutMs to support long-lived idle connections. + MaxReasonableConnectionTimeoutMs = uint32(86400000) // 24 hours in milliseconds // Cipher Suite Validation CipherInvalidChars1 = ";" diff --git a/gateway/gateway-controller/pkg/xds/translator.go b/gateway/gateway-controller/pkg/xds/translator.go index 408a6dda7a..051d681fdf 100644 --- a/gateway/gateway-controller/pkg/xds/translator.go +++ b/gateway/gateway-controller/pkg/xds/translator.go @@ -1042,6 +1042,13 @@ func (t *Translator) createListener(virtualHosts []*route.VirtualHost, isHTTPS b HttpFilters: httpFilters, ServerHeaderTransformation: convertServerHeaderTransformation(t.routerConfig.HTTPListener.ServerHeaderTransformation), ServerName: t.routerConfig.HTTPListener.ServerHeaderValue, + // HCM-level (downstream) timeouts. Defaults match Envoy's documented defaults. + RequestTimeout: durationpb.New(t.routerConfig.HTTPListener.Timeouts.RequestTimeout), + RequestHeadersTimeout: durationpb.New(t.routerConfig.HTTPListener.Timeouts.RequestHeadersTimeout), + StreamIdleTimeout: durationpb.New(t.routerConfig.HTTPListener.Timeouts.StreamIdleTimeout), + CommonHttpProtocolOptions: &core.HttpProtocolOptions{ + IdleTimeout: durationpb.New(t.routerConfig.HTTPListener.Timeouts.IdleTimeout), + }, } // Add access logs if enabled diff --git a/gateway/gateway-controller/pkg/xds/translator_test.go b/gateway/gateway-controller/pkg/xds/translator_test.go index dc637583e8..963ae19504 100644 --- a/gateway/gateway-controller/pkg/xds/translator_test.go +++ b/gateway/gateway-controller/pkg/xds/translator_test.go @@ -31,6 +31,7 @@ import ( core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/stretchr/testify/assert" @@ -948,6 +949,55 @@ func TestTranslator_ExtractProviderName_NilSourceConfig(t *testing.T) { assert.Equal(t, "", result) } +// extractHCM pulls the HttpConnectionManager out of the listener's first filter chain. +func extractHCM(t *testing.T, lis *listener.Listener) *hcm.HttpConnectionManager { + t.Helper() + require.NotEmpty(t, lis.GetFilterChains()) + require.NotEmpty(t, lis.GetFilterChains()[0].GetFilters()) + typedConfig := lis.GetFilterChains()[0].GetFilters()[0].GetTypedConfig() + require.NotNil(t, typedConfig) + manager := &hcm.HttpConnectionManager{} + require.NoError(t, typedConfig.UnmarshalTo(manager)) + return manager +} + +func TestTranslator_CreateListener_HCMTimeouts(t *testing.T) { + tests := []struct { + name string + timeouts config.HCMTimeouts + }{ + { + name: "configured values", + timeouts: config.HCMTimeouts{RequestTimeout: 30 * time.Second, RequestHeadersTimeout: 10 * time.Second, StreamIdleTimeout: 2 * time.Minute, IdleTimeout: 30 * time.Minute}, + }, + { + name: "envoy defaults flow through unchanged", + timeouts: config.HCMTimeouts{RequestTimeout: 0, RequestHeadersTimeout: 0, StreamIdleTimeout: 5 * time.Minute, IdleTimeout: time.Hour}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := createTestLogger() + routerCfg := testRouterConfig() + routerCfg.HTTPListener.Timeouts = tt.timeouts + cfg := testConfig() + cfg.Router = *routerCfg + translator := NewTranslator(logger, routerCfg, nil, cfg) + + lis, _, err := translator.createListener(nil, false) + require.NoError(t, err) + + manager := extractHCM(t, lis) + assert.Equal(t, tt.timeouts.RequestTimeout, manager.GetRequestTimeout().AsDuration(), "request_timeout") + assert.Equal(t, tt.timeouts.RequestHeadersTimeout, manager.GetRequestHeadersTimeout().AsDuration(), "request_headers_timeout") + assert.Equal(t, tt.timeouts.StreamIdleTimeout, manager.GetStreamIdleTimeout().AsDuration(), "stream_idle_timeout") + require.NotNil(t, manager.GetCommonHttpProtocolOptions(), "common_http_protocol_options must be set") + assert.Equal(t, tt.timeouts.IdleTimeout, manager.GetCommonHttpProtocolOptions().GetIdleTimeout().AsDuration(), "idle_timeout") + }) + } +} + func TestTranslator_CreateAccessLogConfig_Disabled(t *testing.T) { // Note: createAccessLogConfig should only be called when access logs are enabled. // The check for enabled is done at the caller level. When called directly with disabled diff --git a/gateway/it/features/backend_timeout.feature b/gateway/it/features/timeouts.feature similarity index 65% rename from gateway/it/features/backend_timeout.feature rename to gateway/it/features/timeouts.feature index 655afa375d..f2ae26afeb 100644 --- a/gateway/it/features/backend_timeout.feature +++ b/gateway/it/features/timeouts.feature @@ -15,11 +15,15 @@ # under the License. # -------------------------------------------------------------------- -@backend-timeout -Feature: Backend timeout +@backend-timeout @timeouts +Feature: Timeouts As an API developer - I want backend timeout (upstreamDefinitions) to be enforced by the gateway - So that requests to slow or unreachable backends fail within the configured timeout + I want upstream (connect) and HTTP Connection Manager timeouts to be enforced by the gateway + So that requests to slow or unreachable backends, and slow downstream clients, + fail within the configured timeout + + # request_timeout, stream_idle_timeout and idle_timeout are not exercised here: + # small values would affect the whole shared suite Background: Given the gateway services are running @@ -97,3 +101,40 @@ Feature: Backend timeout When I delete the API "timeout-api-global-v1.0" Then the response should be successful + # Tests HCM request_headers_timeout (set to "5s" in it/test-config.toml). + # A raw client sends a partial request and never terminates the headers; the gateway + # must close the stream with 408 once request_headers_timeout elapses. + Scenario: HCM request_headers_timeout terminates a slow-header request + Given I authenticate using basic auth as "admin" + When I deploy this API configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: RestApi + metadata: + name: headers-timeout-api-v1.0 + spec: + displayName: Headers-Timeout-API + version: v1.0 + context: /headers-timeout/$version + upstreamDefinitions: + - name: headers-timeout-upstream + upstreams: + - url: http://sample-backend:9080 + upstream: + main: + ref: headers-timeout-upstream + operations: + - method: GET + path: / + """ + Then the response should be successful + And the response should be valid JSON + And the JSON response field "status" should be "success" + And I record the current time as "request_start" + When I open a raw connection to "localhost:8080" and send incomplete request headers for path "/headers-timeout/v1.0/" + Then the raw response status code should be "408" + And the request should have taken at least "4" seconds since "request_start" + Given I authenticate using basic auth as "admin" + When I delete the API "headers-timeout-api-v1.0" + Then the response should be successful + diff --git a/gateway/it/steps_backend_timeout.go b/gateway/it/steps_backend_timeout.go deleted file mode 100644 index c5d8778acd..0000000000 --- a/gateway/it/steps_backend_timeout.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package it - -import ( - "fmt" - "strconv" - "time" - - "github.com/cucumber/godog" -) - -// elapsedTimeToleranceSeconds is the tolerance when asserting minimum elapsed time (e.g. clock skew, scheduling). -const elapsedTimeToleranceSeconds = 1 - -// RegisterBackendTimeoutSteps registers step definitions for backend timeout scenarios -func RegisterBackendTimeoutSteps(ctx *godog.ScenarioContext, state *TestState) { - ctx.Step(`^I record the current time as "([^"]*)"$`, func(key string) error { - state.SetContextValue(key, time.Now()) - return nil - }) - - ctx.Step(`^the request should have taken at least "(\d+)" seconds since "([^"]*)"$`, func(expectedSecondsStr, key string) error { - expectedSeconds, err := strconv.Atoi(expectedSecondsStr) - if err != nil { - return fmt.Errorf("expected seconds must be a number, got: %s", expectedSecondsStr) - } - val, ok := state.GetContextValue(key) - if !ok { - return fmt.Errorf("no start time recorded; record the current time as \"request_start\" before sending the request") - } - start, ok := val.(time.Time) - if !ok { - return fmt.Errorf("no start time recorded; record the current time as %q before sending the request", key) - } - elapsed := time.Since(start) - minElapsed := time.Duration(expectedSeconds-elapsedTimeToleranceSeconds) * time.Second - if elapsed < minElapsed { - return fmt.Errorf("request should have taken at least %d seconds (with %ds tolerance), but elapsed time was %s", - expectedSeconds, elapsedTimeToleranceSeconds, elapsed.Round(time.Millisecond)) - } - return nil - }) -} diff --git a/gateway/it/steps_timeouts.go b/gateway/it/steps_timeouts.go new file mode 100644 index 0000000000..744353fef6 --- /dev/null +++ b/gateway/it/steps_timeouts.go @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package it + +import ( + "fmt" + "io" + "net" + "strconv" + "strings" + "time" + + "github.com/cucumber/godog" +) + +// elapsedTimeToleranceSeconds is the tolerance when asserting minimum elapsed time (e.g. clock skew, scheduling). +const elapsedTimeToleranceSeconds = 1 + +// rawResponseContextKey is where the slow-header raw response is stashed for later assertion. +const rawResponseContextKey = "raw_timeout_response" + +// RegisterTimeoutSteps registers step definitions for upstream and HCM timeout scenarios +func RegisterTimeoutSteps(ctx *godog.ScenarioContext, state *TestState) { + ctx.Step(`^I record the current time as "([^"]*)"$`, func(key string) error { + state.SetContextValue(key, time.Now()) + return nil + }) + + ctx.Step(`^the request should have taken at least "(\d+)" seconds since "([^"]*)"$`, func(expectedSecondsStr, key string) error { + expectedSeconds, err := strconv.Atoi(expectedSecondsStr) + if err != nil { + return fmt.Errorf("expected seconds must be a number, got: %s", expectedSecondsStr) + } + val, ok := state.GetContextValue(key) + if !ok { + return fmt.Errorf("no start time recorded; record the current time as \"request_start\" before sending the request") + } + start, ok := val.(time.Time) + if !ok { + return fmt.Errorf("no start time recorded; record the current time as %q before sending the request", key) + } + elapsed := time.Since(start) + minElapsed := time.Duration(expectedSeconds-elapsedTimeToleranceSeconds) * time.Second + if elapsed < minElapsed { + return fmt.Errorf("request should have taken at least %d seconds (with %ds tolerance), but elapsed time was %s", + expectedSeconds, elapsedTimeToleranceSeconds, elapsed.Round(time.Millisecond)) + } + return nil + }) + + // Opens a raw TCP connection and sends a request line plus one header, but never + // terminates the header block (no final blank line). This forces the HCM + // request_headers_timeout to fire. The connection blocks until the gateway responds + // or closes it, and the raw response is stored for assertion. + ctx.Step(`^I open a raw connection to "([^"]*)" and send incomplete request headers for path "([^"]*)"$`, func(address, path string) error { + conn, err := net.DialTimeout("tcp", address, 10*time.Second) + if err != nil { + return fmt.Errorf("failed to connect to %s: %w", address, err) + } + defer conn.Close() + + // Request line + Host header, with NO terminating CRLF that ends the headers. + partial := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s\r\n", path, address) + if _, err := conn.Write([]byte(partial)); err != nil { + return fmt.Errorf("failed to send partial request: %w", err) + } + + // Read until the gateway responds and closes, bounded by a deadline that is + // comfortably longer than request_headers_timeout so we capture the 408. + if err := conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil { + return fmt.Errorf("failed to set read deadline: %w", err) + } + raw, err := io.ReadAll(conn) + if err != nil && len(raw) == 0 { + return fmt.Errorf("failed to read response from gateway: %w", err) + } + state.SetContextValue(rawResponseContextKey, string(raw)) + return nil + }) + + ctx.Step(`^the raw response status code should be "([^"]*)"$`, func(expectedCode string) error { + val, ok := state.GetContextValue(rawResponseContextKey) + if !ok { + return fmt.Errorf("no raw response recorded; open a raw connection first") + } + raw, ok := val.(string) + if !ok { + return fmt.Errorf("recorded raw response has unexpected type %T", val) + } + statusLine := raw + if idx := strings.Index(raw, "\r\n"); idx >= 0 { + statusLine = raw[:idx] + } + if !strings.Contains(statusLine, expectedCode) { + return fmt.Errorf("expected raw response status line to contain %q, got %q", expectedCode, statusLine) + } + return nil + }) +} diff --git a/gateway/it/suite_test.go b/gateway/it/suite_test.go index 4cbcf0338e..3496ebcd33 100644 --- a/gateway/it/suite_test.go +++ b/gateway/it/suite_test.go @@ -339,7 +339,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) { RegisterMetricsSteps(ctx, testState, httpSteps) RegisterAuthSteps(ctx, testState, httpSteps) RegisterAPISteps(ctx, testState, httpSteps) - RegisterBackendTimeoutSteps(ctx, testState) + RegisterTimeoutSteps(ctx, testState) RegisterMCPSteps(ctx, testState, httpSteps, jwtSteps) RegisterLLMSteps(ctx, testState, httpSteps) RegisterJWTSteps(ctx, testState, httpSteps, jwtSteps) diff --git a/gateway/it/test-config.toml b/gateway/it/test-config.toml index 187d32390f..4833aee646 100644 --- a/gateway/it/test-config.toml +++ b/gateway/it/test-config.toml @@ -72,6 +72,10 @@ enabled = true enabled = true format = "text" +[router.http_listener.timeouts] +# This is the only HCM timeout set to a small, testable value here. +request_headers_timeout = "5s" + # ============================================================================= # POLICY ENGINE CONFIGURATION # ============================================================================= From f37476274190cc2da2229d37a981852104309888 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Wed, 24 Jun 2026 07:13:13 +0530 Subject: [PATCH 2/8] Adding backend timeout support for resiliency --- .../api/management-openapi.yaml | 23 + .../pkg/api/management/generated.go | 565 +++++++++--------- .../pkg/config/api_validator.go | 45 ++ .../pkg/config/api_validator_test.go | 48 ++ .../pkg/models/runtime_deploy_config.go | 7 +- .../pkg/transform/restapi.go | 34 ++ .../pkg/transform/restapi_test.go | 74 +++ .../gateway-controller/pkg/xds/translator.go | 127 +++- .../pkg/xds/translator_test.go | 47 ++ gateway/it/features/backend-timeout.feature | 135 +++++ gateway/it/suite_test.go | 2 + .../api/v1alpha1/restapi_types.go | 25 + .../api/v1alpha1/zz_generated.deepcopy.go | 35 ++ ...ateway.api-platform.wso2.com_restapis.yaml | 30 + .../config/samples/api_v1_restapi.yaml | 7 + ...ateway.api-platform.wso2.com_restapis.yaml | 30 + 16 files changed, 941 insertions(+), 293 deletions(-) create mode 100644 gateway/it/features/backend-timeout.feature diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index f3926b0181..9dbf7d5e5c 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -4194,6 +4194,8 @@ components: description: List of API-level policies applied to all operations unless overridden items: $ref: "#/components/schemas/Policy" + resilience: + $ref: "#/components/schemas/Resilience" operations: type: array description: List of HTTP operations/routes @@ -4258,6 +4260,25 @@ components: pattern: '^\d+(\.\d+)?(ms|s|m|h)$' example: 5s + Resilience: + type: object + description: > + Backend/route timeout configuration. Maps to Envoy RouteAction timeouts. + Can be set at the API level (applies to all routes) and/or the operation level + (applies to that operation's route). When set at both levels, the operation-level + value takes precedence. When unset, the gateway's global route timeout defaults apply. + properties: + timeout: + type: string + description: Maximum time for the entire route (request to upstream response). "0s" disables the timeout. + pattern: '^\d+(\.\d+)?(ms|s|m|h)$' + example: 15s + idleTimeout: + type: string + description: Per-route stream idle timeout (overrides the listener stream idle timeout for this route). "0s" disables the timeout. + pattern: '^\d+(\.\d+)?(ms|s|m|h)$' + example: 0s + Upstream: type: object oneOf: @@ -4313,6 +4334,8 @@ components: description: List of policies applied only to this operation (overrides or adds to API-level policies) items: $ref: "#/components/schemas/Policy" + resilience: + $ref: "#/components/schemas/Resilience" Policy: type: object diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index 5dbc799012..63354575e2 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -504,6 +504,9 @@ type APIConfigData struct { // Policies List of API-level policies applied to all operations unless overridden Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` + // Resilience Backend/route timeout configuration. Maps to Envoy RouteAction timeouts. Can be set at the API level (applies to all routes) and/or the operation level (applies to that operation's route). When set at both levels, the operation-level value takes precedence. When unset, the gateway's global route timeout defaults apply. + Resilience *Resilience `json:"resilience,omitempty" yaml:"resilience,omitempty"` + // SubscriptionPlans List of subscription plan names available for this API SubscriptionPlans *[]string `json:"subscriptionPlans,omitempty" yaml:"subscriptionPlans,omitempty"` @@ -1214,6 +1217,9 @@ type Operation struct { // Policies List of policies applied only to this operation (overrides or adds to API-level policies) Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` + + // Resilience Backend/route timeout configuration. Maps to Envoy RouteAction timeouts. Can be set at the API level (applies to all routes) and/or the operation level (applies to that operation's route). When set at both levels, the operation-level value takes precedence. When unset, the gateway's global route timeout defaults apply. + Resilience *Resilience `json:"resilience,omitempty" yaml:"resilience,omitempty"` } // OperationMethod HTTP method @@ -1255,6 +1261,15 @@ type Policy struct { Version string `json:"version" yaml:"version"` } +// Resilience Backend/route timeout configuration. Maps to Envoy RouteAction timeouts. Can be set at the API level (applies to all routes) and/or the operation level (applies to that operation's route). When set at both levels, the operation-level value takes precedence. When unset, the gateway's global route timeout defaults apply. +type Resilience struct { + // IdleTimeout Per-route stream idle timeout (overrides the listener stream idle timeout for this route). "0s" disables the timeout. + IdleTimeout *string `json:"idleTimeout,omitempty" yaml:"idleTimeout,omitempty"` + + // Timeout Maximum time for the entire route (request to upstream response). "0s" disables the timeout. + Timeout *string `json:"timeout,omitempty" yaml:"timeout,omitempty"` +} + // ResourceStatus Server-managed lifecycle information for a resource type ResourceStatus struct { // CreatedAt Timestamp when the resource was first created (UTC) @@ -5748,281 +5763,281 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/gateway/gateway-controller/pkg/config/api_validator.go b/gateway/gateway-controller/pkg/config/api_validator.go index bb0698f66a..34ba29a49f 100644 --- a/gateway/gateway-controller/pkg/config/api_validator.go +++ b/gateway/gateway-controller/pkg/config/api_validator.go @@ -436,12 +436,54 @@ func (v *APIValidator) validateRestData(spec *api.APIConfigData) []ValidationErr errors = append(errors, v.validateUpstream("sandbox", spec.Upstream.Sandbox, spec.UpstreamDefinitions)...) } + // Validate API-level resilience block + errors = append(errors, v.validateResilience("spec.resilience", spec.Resilience)...) + // Validate operations errors = append(errors, v.validateOperations(spec.Operations)...) return errors } +// validateResilience validates a resilience block (timeout / idleTimeout). Both fields +// are optional duration strings; "0s" is allowed (disables the timeout), negative and +// malformed values are rejected. fieldPrefix is the path to the block (e.g. +// "spec.resilience" or "spec.operations[2].resilience"). +func (v *APIValidator) validateResilience(fieldPrefix string, r *api.Resilience) []ValidationError { + var errors []ValidationError + if r == nil { + return errors + } + + validate := func(field string, value *string) { + if value == nil { + return + } + s := strings.TrimSpace(*value) + if s == "" { + return + } + d, err := time.ParseDuration(s) + if err != nil { + errors = append(errors, ValidationError{ + Field: field, + Message: fmt.Sprintf("Invalid timeout format: %v (expected format: '30s', '1m', '500ms', or '0s' to disable)", err), + }) + return + } + if d < 0 { + errors = append(errors, ValidationError{ + Field: field, + Message: "Timeout must not be negative", + }) + } + } + + validate(fieldPrefix+".timeout", r.Timeout) + validate(fieldPrefix+".idleTimeout", r.IdleTimeout) + return errors +} + // validateAsyncData validates the data section of the configuration for http/rest kind func (v *APIValidator) validateAsyncData(spec *api.WebhookAPIData) []ValidationError { var errors []ValidationError @@ -621,6 +663,9 @@ func (v *APIValidator) validateOperations(operations []api.Operation) []Validati Message: "Operation path has unbalanced braces in parameters", }) } + + // Validate operation-level resilience block + errors = append(errors, v.validateResilience(fmt.Sprintf("spec.operations[%d].resilience", i), op.Resilience)...) } return errors diff --git a/gateway/gateway-controller/pkg/config/api_validator_test.go b/gateway/gateway-controller/pkg/config/api_validator_test.go index f93a0d58d4..ec8c32cf55 100644 --- a/gateway/gateway-controller/pkg/config/api_validator_test.go +++ b/gateway/gateway-controller/pkg/config/api_validator_test.go @@ -478,6 +478,54 @@ func TestAPIValidator_ValidateOperations(t *testing.T) { } } +func TestAPIValidator_ValidateResilience(t *testing.T) { + v := NewAPIValidator() + + tests := []struct { + name string + apiRes *api.Resilience + opRes *api.Resilience + wantError bool + errField string + }{ + {name: "No resilience is valid", wantError: false}, + {name: "Valid API-level timeout + idleTimeout", apiRes: &api.Resilience{Timeout: stringPtr("15s"), IdleTimeout: stringPtr("0s")}, wantError: false}, + {name: "Valid operation-level timeout", opRes: &api.Resilience{Timeout: stringPtr("2s")}, wantError: false}, + {name: "0s is allowed (disabled)", apiRes: &api.Resilience{Timeout: stringPtr("0s"), IdleTimeout: stringPtr("0s")}, wantError: false}, + {name: "Invalid API-level timeout format", apiRes: &api.Resilience{Timeout: stringPtr("15seconds")}, wantError: true, errField: "spec.resilience.timeout"}, + {name: "Invalid API-level idleTimeout format", apiRes: &api.Resilience{IdleTimeout: stringPtr("abc")}, wantError: true, errField: "spec.resilience.idleTimeout"}, + {name: "Negative API-level timeout rejected", apiRes: &api.Resilience{Timeout: stringPtr("-5s")}, wantError: true, errField: "spec.resilience.timeout"}, + {name: "Invalid operation-level timeout format", opRes: &api.Resilience{Timeout: stringPtr("nope")}, wantError: true, errField: "spec.operations[0].resilience.timeout"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := createValidRestAPIConfig() + cfg.Spec.Resilience = tt.apiRes + cfg.Spec.Operations[0].Resilience = tt.opRes + + errors := v.Validate(cfg) + hasExpectedError := false + for _, e := range errors { + if e.Field == tt.errField { + hasExpectedError = true + break + } + } + if tt.wantError && !hasExpectedError { + t.Errorf("expected error for field %s, got: %v", tt.errField, errors) + } + if !tt.wantError { + for _, e := range errors { + if strings.Contains(e.Field, "resilience") { + t.Errorf("unexpected resilience error: %v", e) + } + } + } + }) + } +} + func TestAPIValidator_ValidateAllHTTPMethods(t *testing.T) { v := NewAPIValidator() diff --git a/gateway/gateway-controller/pkg/models/runtime_deploy_config.go b/gateway/gateway-controller/pkg/models/runtime_deploy_config.go index 8dec6fa8c7..e5111228ec 100644 --- a/gateway/gateway-controller/pkg/models/runtime_deploy_config.go +++ b/gateway/gateway-controller/pkg/models/runtime_deploy_config.go @@ -62,8 +62,13 @@ type Route struct { } // RouteTimeout holds parsed timeout values for a route. +// Timeout and IdleTimeout come from the resilience block (operation-level overriding +// API-level). A nil field means "not configured" — the global route timeout default +// applies. A non-nil zero value means "explicitly disabled". type RouteTimeout struct { - Connect *time.Duration + Connect *time.Duration + Timeout *time.Duration // route timeout -> RouteAction.Timeout + IdleTimeout *time.Duration // route idle timeout -> RouteAction.IdleTimeout } // RouteUpstream links a route to its upstream cluster. diff --git a/gateway/gateway-controller/pkg/transform/restapi.go b/gateway/gateway-controller/pkg/transform/restapi.go index 8d404dca3d..053fdb3223 100644 --- a/gateway/gateway-controller/pkg/transform/restapi.go +++ b/gateway/gateway-controller/pkg/transform/restapi.go @@ -24,6 +24,7 @@ import ( "net/url" "strconv" "strings" + "time" commonconstants "github.com/wso2/api-platform/common/constants" versionutil "github.com/wso2/api-platform/common/version" @@ -151,8 +152,22 @@ func (t *RestAPITransformer) Transform(cfg *models.StoredConfig) (*models.Runtim return nil, fmt.Errorf("sandbox upstream is configured but resolves to the same vhost %q as the main upstream; configure distinct vhosts to avoid route conflicts", effectiveMainVHost) } + // Resolve API-level resilience timeouts once; operation-level values override these. + apiTimeout, apiIdleTimeout, err := xds.ResolveResilience(apiData.Resilience) + if err != nil { + return nil, fmt.Errorf("invalid API-level resilience: %w", err) + } + // Build routes and policy chains for each operation for _, op := range apiData.Operations { + // Operation-level resilience overrides API-level (per field); nil leaves the + // global route timeout default in effect. + opTimeout, opIdleTimeout, err := xds.ResolveResilience(op.Resilience) + if err != nil { + return nil, fmt.Errorf("invalid resilience for operation %s %s: %w", op.Method, op.Path, err) + } + routeTimeout := buildRouteTimeout(opTimeout, apiTimeout, opIdleTimeout, apiIdleTimeout) + vhosts := []string{effectiveMainVHost} if hasSandbox { vhosts = append(vhosts, effectiveSandboxVHost) @@ -168,6 +183,7 @@ func (t *RestAPITransformer) Transform(cfg *models.StoredConfig) (*models.Runtim OperationPath: op.Path, Vhost: vhost, AutoHostRewrite: mainAutoHostRewrite, + Timeout: routeTimeout, Upstream: models.RouteUpstream{ ClusterKey: mainUpstream.ClusterKey, UseClusterHeader: useClusterHeader, @@ -246,6 +262,24 @@ func (t *RestAPITransformer) Transform(cfg *models.StoredConfig) (*models.Runtim } // collectAPIPolicies validates and collects API-level policies into SDK format. +// buildRouteTimeout applies operation-over-API precedence (per field) and returns a +// *models.RouteTimeout, or nil when neither level configured any timeout (so the global +// route timeout default applies). +func buildRouteTimeout(opTimeout, apiTimeout, opIdle, apiIdle *time.Duration) *models.RouteTimeout { + timeout := opTimeout + if timeout == nil { + timeout = apiTimeout + } + idle := opIdle + if idle == nil { + idle = apiIdle + } + if timeout == nil && idle == nil { + return nil + } + return &models.RouteTimeout{Timeout: timeout, IdleTimeout: idle} +} + func (t *RestAPITransformer) collectAPIPolicies(policies *[]api.Policy) map[string]policyenginev1.PolicyInstance { result := make(map[string]policyenginev1.PolicyInstance) if policies == nil { diff --git a/gateway/gateway-controller/pkg/transform/restapi_test.go b/gateway/gateway-controller/pkg/transform/restapi_test.go index 5edf451df9..2f5835715c 100644 --- a/gateway/gateway-controller/pkg/transform/restapi_test.go +++ b/gateway/gateway-controller/pkg/transform/restapi_test.go @@ -21,6 +21,7 @@ package transform import ( "net/url" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -84,6 +85,79 @@ func makeRestAPIStoredConfig(apiPolicies []api.Policy, opPolicies []api.Policy) } } +// makeRestAPIStoredConfigWithResilience builds a RestAPI StoredConfig whose single +// operation (GET /hello) carries optional API-level and operation-level resilience blocks. +func makeRestAPIStoredConfigWithResilience(apiRes, opRes *api.Resilience) *models.StoredConfig { + apiData := api.APIConfigData{ + DisplayName: "Test API", + Context: "/test", + Version: "1.0.0", + Resilience: apiRes, + Operations: []api.Operation{ + {Method: "GET", Path: "/hello", Resilience: opRes}, + }, + Upstream: struct { + Main api.Upstream `json:"main" yaml:"main"` + Sandbox *api.Upstream `json:"sandbox,omitempty" yaml:"sandbox,omitempty"` + }{ + Main: api.Upstream{Url: ptrStr("http://backend:8080")}, + }, + } + return &models.StoredConfig{ + UUID: "test-api", + Kind: string(api.RestAPIKindRestApi), + Configuration: api.RestAPI{Kind: api.RestAPIKindRestApi, Metadata: api.Metadata{Name: "test-api"}, Spec: apiData}, + } +} + +func TestRestAPITransformer_ResiliencePrecedence(t *testing.T) { + const routeKey = "GET|/test/hello|main.local" + transformer := NewRestAPITransformer(testRouterCfg(), &config.Config{}, map[string]models.PolicyDefinition{}) + + t.Run("operation-level overrides API-level", func(t *testing.T) { + cfg := makeRestAPIStoredConfigWithResilience( + &api.Resilience{Timeout: ptrStr("10s"), IdleTimeout: ptrStr("30s")}, + &api.Resilience{Timeout: ptrStr("2s")}, + ) + rdc, err := transformer.Transform(cfg) + require.NoError(t, err) + rt := rdc.Routes[routeKey].Timeout + require.NotNil(t, rt) + require.NotNil(t, rt.Timeout) + assert.Equal(t, 2*time.Second, *rt.Timeout, "operation timeout should win") + require.NotNil(t, rt.IdleTimeout) + assert.Equal(t, 30*time.Second, *rt.IdleTimeout, "idleTimeout falls back to API-level when op omits it") + }) + + t.Run("API-level applies when operation omits resilience", func(t *testing.T) { + cfg := makeRestAPIStoredConfigWithResilience(&api.Resilience{Timeout: ptrStr("7s")}, nil) + rdc, err := transformer.Transform(cfg) + require.NoError(t, err) + rt := rdc.Routes[routeKey].Timeout + require.NotNil(t, rt) + require.NotNil(t, rt.Timeout) + assert.Equal(t, 7*time.Second, *rt.Timeout) + assert.Nil(t, rt.IdleTimeout) + }) + + t.Run("no resilience leaves Timeout nil (global default applies)", func(t *testing.T) { + cfg := makeRestAPIStoredConfigWithResilience(nil, nil) + rdc, err := transformer.Transform(cfg) + require.NoError(t, err) + assert.Nil(t, rdc.Routes[routeKey].Timeout) + }) + + t.Run("0s is preserved as explicit disable", func(t *testing.T) { + cfg := makeRestAPIStoredConfigWithResilience(&api.Resilience{Timeout: ptrStr("0s")}, nil) + rdc, err := transformer.Transform(cfg) + require.NoError(t, err) + rt := rdc.Routes[routeKey].Timeout + require.NotNil(t, rt) + require.NotNil(t, rt.Timeout) + assert.Equal(t, time.Duration(0), *rt.Timeout) + }) +} + // findPolicyInChain returns true if any policy chain for the given route key // contains a policy with the given name. func findPolicyInChain(rdc *models.RuntimeDeployConfig, routeKey, policyName string) bool { diff --git a/gateway/gateway-controller/pkg/xds/translator.go b/gateway/gateway-controller/pkg/xds/translator.go index 051d681fdf..6140529dcb 100644 --- a/gateway/gateway-controller/pkg/xds/translator.go +++ b/gateway/gateway-controller/pkg/xds/translator.go @@ -96,9 +96,11 @@ type Translator struct { } // resolvedTimeout represents parsed timeout values for an upstream. -// Currently only connect timeout is supported at the upstream definition level. +// Route and Idle come from the resilience block. type resolvedTimeout struct { Connect *time.Duration + Route *time.Duration + Idle *time.Duration } // NewTranslator creates a new translator @@ -214,6 +216,16 @@ func (t *Translator) translateRuntimeConfig(rdc *models.RuntimeDeployConfig) ([] return routes, clusters, nil } +// routeTimeoutOrDefault returns the per-route timeout when configured (including an +// explicit zero, which disables the timeout in Envoy), otherwise the global default +// expressed in milliseconds. +func (t *Translator) routeTimeoutOrDefault(v *time.Duration, defaultMs uint32) *durationpb.Duration { + if v != nil { + return durationpb.New(*v) + } + return durationpb.New(time.Duration(defaultMs) * time.Millisecond) +} + // createRouteFromRDC creates an Envoy route from a RuntimeDeployConfig Route. func (t *Translator) createRouteFromRDC(routeKey string, rdcRoute *models.Route, rdc *models.RuntimeDeployConfig) *route.Route { fullPath := rdcRoute.Path @@ -249,15 +261,17 @@ func (t *Translator) createRouteFromRDC(routeKey string, rdcRoute *models.Route, } } - // Build route action with timeouts + // Build route action with timeouts. Per-route resilience values (from the API/operation + // resilience block) take precedence; otherwise fall back to the global route defaults. + var routeResilienceTimeout, routeResilienceIdle *time.Duration + if rdcRoute.Timeout != nil { + routeResilienceTimeout = rdcRoute.Timeout.Timeout + routeResilienceIdle = rdcRoute.Timeout.IdleTimeout + } routeAction := &route.Route_Route{ Route: &route.RouteAction{ - Timeout: durationpb.New( - time.Duration(t.routerConfig.Upstream.Timeouts.RouteTimeoutMs) * time.Millisecond, - ), - IdleTimeout: durationpb.New( - time.Duration(t.routerConfig.Upstream.Timeouts.RouteIdleTimeoutMs) * time.Millisecond, - ), + Timeout: t.routeTimeoutOrDefault(routeResilienceTimeout, t.routerConfig.Upstream.Timeouts.RouteTimeoutMs), + IdleTimeout: t.routeTimeoutOrDefault(routeResilienceIdle, t.routerConfig.Upstream.Timeouts.RouteIdleTimeoutMs), }, } @@ -818,13 +832,25 @@ func (t *Translator) translateAPIConfig(cfg *models.StoredConfig, allConfigs []* } } + // Resolve API-level resilience timeouts once; operation-level values override per field. + apiTimeout, apiIdleTimeout, err := ResolveResilience(apiData.Resilience) + if err != nil { + return nil, nil, fmt.Errorf("invalid API-level resilience: %w", err) + } + for _, op := range apiData.Operations { // Determine if dynamic cluster selection should be used // When upstreamDefinitions exist, use cluster_header routing so policies can select the upstream useClusterHeader := apiData.UpstreamDefinitions != nil && len(*apiData.UpstreamDefinitions) > 0 + opTimeout, opIdleTimeout, err := ResolveResilience(op.Resilience) + if err != nil { + return nil, nil, fmt.Errorf("invalid resilience for operation %s %s: %w", op.Method, op.Path, err) + } + opTimeoutCfg := combineRouteResilience(mainTimeout, apiTimeout, apiIdleTimeout, opTimeout, opIdleTimeout) + r := t.createRoute(cfg.UUID, apiData.DisplayName, apiData.Version, apiData.Context, string(op.Method), op.Path, - mainClusterName, parsedMainURL.Path, effectiveMainVHost, cfg.Kind, templateHandle, providerName, apiData.Upstream.Main.HostRewrite, apiProjectID, mainTimeout, useClusterHeader, upstreamDefPaths) + mainClusterName, parsedMainURL.Path, effectiveMainVHost, cfg.Kind, templateHandle, providerName, apiData.Upstream.Main.HostRewrite, apiProjectID, opTimeoutCfg, useClusterHeader, upstreamDefPaths) mainRoutesList = append(mainRoutesList, r) } routesList = append(routesList, mainRoutesList...) @@ -850,8 +876,14 @@ func (t *Translator) translateAPIConfig(cfg *models.StoredConfig, allConfigs []* sbRoutesList := make([]*route.Route, 0) sbUseClusterHeader := apiData.UpstreamDefinitions != nil && len(*apiData.UpstreamDefinitions) > 0 for _, op := range apiData.Operations { + opTimeout, opIdleTimeout, err := ResolveResilience(op.Resilience) + if err != nil { + return nil, nil, fmt.Errorf("invalid resilience for operation %s %s: %w", op.Method, op.Path, err) + } + opTimeoutCfg := combineRouteResilience(sbTimeout, apiTimeout, apiIdleTimeout, opTimeout, opIdleTimeout) + r := t.createRoute(cfg.UUID, apiData.DisplayName, apiData.Version, apiData.Context, string(op.Method), op.Path, - sbClusterName, parsedSbURL.Path, effectiveSandboxVHost, cfg.Kind, templateHandle, providerName, apiData.Upstream.Sandbox.HostRewrite, apiProjectID, sbTimeout, sbUseClusterHeader, upstreamDefPaths) + sbClusterName, parsedSbURL.Path, effectiveSandboxVHost, cfg.Kind, templateHandle, providerName, apiData.Upstream.Sandbox.HostRewrite, apiProjectID, opTimeoutCfg, sbUseClusterHeader, upstreamDefPaths) sbRoutesList = append(sbRoutesList, r) } routesList = append(routesList, sbRoutesList...) @@ -1798,15 +1830,17 @@ func (t *Translator) createRoute(apiId, apiName, apiVersion, context, method, pa } } - // Currently the route level timeouts are configurable using the global configuration. + // Route-level timeouts: per-route resilience values (resilience block) take precedence, + // otherwise fall back to the global configuration defaults. + var routeTimeout, routeIdleTimeout *time.Duration + if timeoutCfg != nil { + routeTimeout = timeoutCfg.Route + routeIdleTimeout = timeoutCfg.Idle + } routeAction := &route.Route_Route{ Route: &route.RouteAction{ - Timeout: durationpb.New( - time.Duration(t.routerConfig.Upstream.Timeouts.RouteTimeoutMs) * time.Millisecond, - ), - IdleTimeout: durationpb.New( - time.Duration(t.routerConfig.Upstream.Timeouts.RouteIdleTimeoutMs) * time.Millisecond, - ), + Timeout: t.routeTimeoutOrDefault(routeTimeout, t.routerConfig.Upstream.Timeouts.RouteTimeoutMs), + IdleTimeout: t.routeTimeoutOrDefault(routeIdleTimeout, t.routerConfig.Upstream.Timeouts.RouteIdleTimeoutMs), }, } @@ -3041,6 +3075,65 @@ func parseTimeout(timeoutStr *string) (*time.Duration, error) { return &duration, nil } +// parseDurationAllowZero parses a duration string (e.g. "15s", "0s") into a *time.Duration. +// Unlike parseTimeout it accepts zero ("0s" means the timeout is explicitly disabled), +// rejecting only malformed and negative values. Returns nil for nil/empty input. +func parseDurationAllowZero(timeoutStr *string) (*time.Duration, error) { + if timeoutStr == nil || strings.TrimSpace(*timeoutStr) == "" { + return nil, nil + } + + duration, err := time.ParseDuration(strings.TrimSpace(*timeoutStr)) + if err != nil { + return nil, fmt.Errorf("invalid timeout format: %w", err) + } + + if duration < 0 { + return nil, fmt.Errorf("timeout must not be negative, got: %v", duration) + } + + return &duration, nil +} + +// ResolveResilience parses a resilience block into route timeout and idle-timeout durations. +// A nil block, or unset fields, yield nil durations (meaning "use the global default"). +// "0s" yields a non-nil zero duration (meaning "explicitly disabled"). +func ResolveResilience(r *api.Resilience) (timeout *time.Duration, idleTimeout *time.Duration, err error) { + if r == nil { + return nil, nil, nil + } + if timeout, err = parseDurationAllowZero(r.Timeout); err != nil { + return nil, nil, fmt.Errorf("invalid resilience.timeout: %w", err) + } + if idleTimeout, err = parseDurationAllowZero(r.IdleTimeout); err != nil { + return nil, nil, fmt.Errorf("invalid resilience.idleTimeout: %w", err) + } + return timeout, idleTimeout, nil +} + +// combineRouteResilience returns a resolvedTimeout for a single route, preserving the +// upstream connect timeout from base and applying the effective route/idle timeouts +// (operation-level overriding API-level, per field). It returns base unchanged when no +// resilience is configured at either level. +func combineRouteResilience(base *resolvedTimeout, apiTimeout, apiIdle, opTimeout, opIdle *time.Duration) *resolvedTimeout { + effTimeout := opTimeout + if effTimeout == nil { + effTimeout = apiTimeout + } + effIdle := opIdle + if effIdle == nil { + effIdle = apiIdle + } + if effTimeout == nil && effIdle == nil { + return base + } + rt := resolvedTimeout{Route: effTimeout, Idle: effIdle} + if base != nil { + rt.Connect = base.Connect + } + return &rt +} + // resolveTimeoutFromDefinition converts an UpstreamDefinition's timeout block into a resolvedTimeout. // Returns nil if there is no timeout block or all fields are empty. func resolveTimeoutFromDefinition(def *api.UpstreamDefinition) (*resolvedTimeout, error) { diff --git a/gateway/gateway-controller/pkg/xds/translator_test.go b/gateway/gateway-controller/pkg/xds/translator_test.go index 963ae19504..2a70fe5715 100644 --- a/gateway/gateway-controller/pkg/xds/translator_test.go +++ b/gateway/gateway-controller/pkg/xds/translator_test.go @@ -739,6 +739,53 @@ func TestTranslator_WildcardUpstreamRewriteFromRDC(t *testing.T) { } } +// TestTranslator_RouteResilienceTimeoutsFromRDC verifies that per-route resilience +// timeouts on a models.Route flow into the Envoy RouteAction, with fallback to the +// global defaults (60s / 300s from testRouterConfig) when unset, and that an explicit +// 0s is preserved (disables the timeout). +func TestTranslator_RouteResilienceTimeoutsFromRDC(t *testing.T) { + logger := createTestLogger() + routerCfg := testRouterConfig() + cfg := testConfig() + translator := NewTranslator(logger, routerCfg, nil, cfg) + + dur := func(d time.Duration) *time.Duration { return &d } + + tests := []struct { + name string + timeout *models.RouteTimeout + wantTimeout time.Duration + wantIdle time.Duration + }{ + {name: "nil timeout uses global defaults", timeout: nil, wantTimeout: 60 * time.Second, wantIdle: 300 * time.Second}, + {name: "configured values applied", timeout: &models.RouteTimeout{Timeout: dur(2 * time.Second), IdleTimeout: dur(10 * time.Second)}, wantTimeout: 2 * time.Second, wantIdle: 10 * time.Second}, + {name: "timeout set, idle falls back", timeout: &models.RouteTimeout{Timeout: dur(3 * time.Second)}, wantTimeout: 3 * time.Second, wantIdle: 300 * time.Second}, + {name: "explicit 0s disables route timeout", timeout: &models.RouteTimeout{Timeout: dur(0)}, wantTimeout: 0, wantIdle: 300 * time.Second}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rdc := &models.RuntimeDeployConfig{ + UpstreamClusters: map[string]*models.UpstreamCluster{ + "main": {Endpoints: []models.Endpoint{{Host: "echo", Port: 80}}}, + }, + } + rdcRoute := &models.Route{ + Method: "GET", + Path: "/api/v1.0/items", + OperationPath: "/items", + AutoHostRewrite: true, + Timeout: tt.timeout, + Upstream: models.RouteUpstream{ClusterKey: "main"}, + } + r := translator.createRouteFromRDC("GET|/api/v1.0/items|", rdcRoute, rdc) + require.NotNil(t, r) + assert.Equal(t, tt.wantTimeout, r.GetRoute().GetTimeout().AsDuration(), "route timeout") + assert.Equal(t, tt.wantIdle, r.GetRoute().GetIdleTimeout().AsDuration(), "route idle timeout") + }) + } +} + func TestTranslator_SanitizeClusterName(t *testing.T) { logger := createTestLogger() routerCfg := testRouterConfig() diff --git a/gateway/it/features/backend-timeout.feature b/gateway/it/features/backend-timeout.feature new file mode 100644 index 0000000000..031a28ca86 --- /dev/null +++ b/gateway/it/features/backend-timeout.feature @@ -0,0 +1,135 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# -------------------------------------------------------------------- + +@backend-timeout @resilience +Feature: Backend route timeouts via the resilience block + As an API developer + I want to configure the route timeout via a resilience block at the API and operation level + So that requests to slow backends are terminated by the gateway within the configured time + + # These scenarios exercise resilience.timeout (Envoy RouteAction.Timeout). The slow + # backend is httpbin (echo-backend) /delay/{n}, which sleeps n seconds before responding; + # when the route timeout is shorter than the delay, the gateway returns 504. + # resilience.idleTimeout maps to RouteAction.IdleTimeout and is covered by unit tests + # (it cannot be exercised deterministically over HTTP here). + + Background: + Given the gateway services are running + + # API-level resilience.timeout (2s) is shorter than the backend delay (5s), so the + # gateway must time the route out with 504 at ~2s instead of waiting for the backend. + Scenario: API-level resilience timeout terminates a slow backend + Given I authenticate using basic auth as "admin" + When I deploy this API configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: RestApi + metadata: + name: backend-timeout-api-v1.0 + spec: + displayName: Backend-Timeout-API + version: v1.0 + context: /backend-timeout-api/$version + upstream: + main: + url: http://echo-backend:80 + resilience: + timeout: 2s + operations: + - method: GET + path: /get + - method: GET + path: /delay/5 + """ + Then the response should be successful + And I wait for the endpoint "http://localhost:8080/backend-timeout-api/v1.0/get" to be ready + And I record the current time as "request_start" + When I send a GET request to "http://localhost:8080/backend-timeout-api/v1.0/delay/5" + Then the response status code should be 504 + And the request should have taken at least "2" seconds since "request_start" + Given I authenticate using basic auth as "admin" + When I delete the API "backend-timeout-api-v1.0" + Then the response should be successful + + # Operation-level resilience overrides the API level: the API-level timeout (10s) is + # longer than the backend delay (5s) and would let the request succeed, but the + # operation-level timeout (2s) wins, so the gateway returns 504 at ~2s. + Scenario: Operation-level resilience timeout overrides the API level + Given I authenticate using basic auth as "admin" + When I deploy this API configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: RestApi + metadata: + name: backend-timeout-override-v1.0 + spec: + displayName: Backend-Timeout-Override + version: v1.0 + context: /backend-timeout-override/$version + upstream: + main: + url: http://echo-backend:80 + resilience: + timeout: 10s + operations: + - method: GET + path: /get + - method: GET + path: /delay/5 + resilience: + timeout: 2s + """ + Then the response should be successful + And I wait for the endpoint "http://localhost:8080/backend-timeout-override/v1.0/get" to be ready + And I record the current time as "request_start" + When I send a GET request to "http://localhost:8080/backend-timeout-override/v1.0/delay/5" + Then the response status code should be 504 + And the request should have taken at least "2" seconds since "request_start" + Given I authenticate using basic auth as "admin" + When I delete the API "backend-timeout-override-v1.0" + Then the response should be successful + + # Without a resilience block the global route timeout default (60s) applies, so a + # backend that responds within it succeeds normally. + Scenario: No resilience block falls back to the global default and succeeds + Given I authenticate using basic auth as "admin" + When I deploy this API configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: RestApi + metadata: + name: backend-timeout-default-v1.0 + spec: + displayName: Backend-Timeout-Default + version: v1.0 + context: /backend-timeout-default/$version + upstream: + main: + url: http://echo-backend:80 + operations: + - method: GET + path: /get + - method: GET + path: /delay/2 + """ + Then the response should be successful + And I wait for the endpoint "http://localhost:8080/backend-timeout-default/v1.0/get" to be ready + When I send a GET request to "http://localhost:8080/backend-timeout-default/v1.0/delay/2" + Then the response status code should be 200 + Given I authenticate using basic auth as "admin" + When I delete the API "backend-timeout-default-v1.0" + Then the response should be successful diff --git a/gateway/it/suite_test.go b/gateway/it/suite_test.go index 3496ebcd33..0e1df34ac0 100644 --- a/gateway/it/suite_test.go +++ b/gateway/it/suite_test.go @@ -143,6 +143,8 @@ func getFeaturePaths() []string { "features/route-path-matching.feature", "features/secrets.feature", "features/template-functions.feature", + "features/timeouts.feature", + "features/backend-timeout.feature", // Runs late: it restarts the gateway-controller (reject/reconnect scenario), so keep it // after features that assume an uninterrupted controller. Verifies the DP->CP artifact push. "features/dp-to-cp.feature", diff --git a/kubernetes/gateway-operator/api/v1alpha1/restapi_types.go b/kubernetes/gateway-operator/api/v1alpha1/restapi_types.go index 6077eb981f..a24bf15b78 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/restapi_types.go +++ b/kubernetes/gateway-operator/api/v1alpha1/restapi_types.go @@ -74,6 +74,11 @@ type APIConfigData struct { // +optional Policies []Policy `json:"policies,omitempty"` + // Resilience API-level backend/route timeout configuration applied to all operations + // unless overridden at the operation level + // +optional + Resilience *Resilience `json:"resilience,omitempty"` + // Upstream API-level upstream configuration // +kubebuilder:validation:Required Upstream UpstreamConfig `json:"upstream"` @@ -125,6 +130,26 @@ type Operation struct { // Policies List of policies applied only to this operation (overrides or adds to API-level policies) // +optional Policies []Policy `json:"policies,omitempty"` + + // Resilience Operation-level backend/route timeout configuration (overrides API-level) + // +optional + Resilience *Resilience `json:"resilience,omitempty"` +} + +// Resilience defines backend/route timeout configuration (maps to Envoy RouteAction +// timeouts). Settable at the API level (applies to all routes) and/or the operation level +// (overrides the API level). "0s" disables a timeout; unset falls back to the gateway's +// global route timeout defaults. +type Resilience struct { + // Timeout Maximum time for the entire route (request to upstream response). "0s" disables. + // +optional + // +kubebuilder:validation:Pattern=`^\d+(\.\d+)?(ms|s|m|h)$` + Timeout *string `json:"timeout,omitempty"` + + // IdleTimeout Per-route stream idle timeout. "0s" disables. + // +optional + // +kubebuilder:validation:Pattern=`^\d+(\.\d+)?(ms|s|m|h)$` + IdleTimeout *string `json:"idleTimeout,omitempty"` } // OperationMethod HTTP method diff --git a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go index b6163d5fd5..14c78bcddc 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -43,6 +43,11 @@ func (in *APIConfigData) DeepCopyInto(out *APIConfigData) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Resilience != nil { + in, out := &in.Resilience, &out.Resilience + *out = new(Resilience) + (*in).DeepCopyInto(*out) + } in.Upstream.DeepCopyInto(&out.Upstream) if in.Vhosts != nil { in, out := &in.Vhosts, &out.Vhosts @@ -1587,6 +1592,11 @@ func (in *Operation) DeepCopyInto(out *Operation) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Resilience != nil { + in, out := &in.Resilience, &out.Resilience + *out = new(Resilience) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operation. @@ -1624,6 +1634,31 @@ func (in *Policy) DeepCopy() *Policy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Resilience) DeepCopyInto(out *Resilience) { + *out = *in + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(string) + **out = **in + } + if in.IdleTimeout != nil { + in, out := &in.IdleTimeout, &out.IdleTimeout + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resilience. +func (in *Resilience) DeepCopy() *Resilience { + if in == nil { + return nil + } + out := new(Resilience) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { *out = *in diff --git a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_restapis.yaml b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_restapis.yaml index 3d1ad1a2aa..4e62b044a0 100644 --- a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_restapis.yaml +++ b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_restapis.yaml @@ -98,6 +98,21 @@ spec: - version type: object type: array + resilience: + description: Resilience Operation-level backend/route timeout + configuration (overrides API-level) + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. + "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object required: - method - path @@ -132,6 +147,21 @@ spec: - version type: object type: array + resilience: + description: |- + Resilience API-level backend/route timeout configuration applied to all operations + unless overridden at the operation level + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object upstream: description: Upstream API-level upstream configuration properties: diff --git a/kubernetes/gateway-operator/config/samples/api_v1_restapi.yaml b/kubernetes/gateway-operator/config/samples/api_v1_restapi.yaml index 750e88cf08..74ce33658d 100644 --- a/kubernetes/gateway-operator/config/samples/api_v1_restapi.yaml +++ b/kubernetes/gateway-operator/config/samples/api_v1_restapi.yaml @@ -21,11 +21,18 @@ spec: # params: # issuers: # - WSO2KeyManager1 + # API-level resilience applies to all operations unless overridden per operation. + resilience: + timeout: 15s + idleTimeout: 0s operations: - method: GET path: /info - method: POST path: /submit + # Operation-level resilience overrides the API-level values for this route. + resilience: + timeout: 5s - method: GET path: /helloworld - method: GET diff --git a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_restapis.yaml b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_restapis.yaml index 3d1ad1a2aa..4e62b044a0 100644 --- a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_restapis.yaml +++ b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_restapis.yaml @@ -98,6 +98,21 @@ spec: - version type: object type: array + resilience: + description: Resilience Operation-level backend/route timeout + configuration (overrides API-level) + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. + "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object required: - method - path @@ -132,6 +147,21 @@ spec: - version type: object type: array + resilience: + description: |- + Resilience API-level backend/route timeout configuration applied to all operations + unless overridden at the operation level + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object upstream: description: Upstream API-level upstream configuration properties: From 04acdf5ccbdbabbd5a376d4a41eb283c5d3d785f Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Thu, 25 Jun 2026 11:00:50 +0530 Subject: [PATCH 3/8] Update build manifest and test file rename --- .../{timeouts.feature => upstream-connect-timeout.feature} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gateway/it/features/{timeouts.feature => upstream-connect-timeout.feature} (100%) diff --git a/gateway/it/features/timeouts.feature b/gateway/it/features/upstream-connect-timeout.feature similarity index 100% rename from gateway/it/features/timeouts.feature rename to gateway/it/features/upstream-connect-timeout.feature From 2c9e7e7b2f9c1ebdf8d0742eb6dd6533966a9295 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Thu, 25 Jun 2026 11:58:16 +0530 Subject: [PATCH 4/8] Update generated.go --- .../pkg/api/management/generated.go | 554 +++++++++--------- 1 file changed, 279 insertions(+), 275 deletions(-) diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index 63354575e2..375822d2be 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -5763,281 +5763,285 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file From 93ff89902c068802db255baa272883b52a12745a Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Thu, 25 Jun 2026 12:11:38 +0530 Subject: [PATCH 5/8] update test feature list to match the new one --- gateway/it/suite_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/it/suite_test.go b/gateway/it/suite_test.go index 0e1df34ac0..f8c017122c 100644 --- a/gateway/it/suite_test.go +++ b/gateway/it/suite_test.go @@ -143,7 +143,7 @@ func getFeaturePaths() []string { "features/route-path-matching.feature", "features/secrets.feature", "features/template-functions.feature", - "features/timeouts.feature", + "features/upstream-connect-timeout.feature", "features/backend-timeout.feature", // Runs late: it restarts the gateway-controller (reject/reconnect scenario), so keep it // after features that assume an uninterrupted controller. Verifies the DP->CP artifact push. From 53221ac20bc31a0463b43a411ce6c790bd4c04e1 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Thu, 25 Jun 2026 16:07:49 +0530 Subject: [PATCH 6/8] Adding request timeout support for llmprovider and proxy --- .../api/management-openapi.yaml | 14 + .../pkg/api/management/generated.go | 444 +++++++++--------- .../pkg/config/api_validator.go | 5 + .../pkg/config/llm_validator.go | 8 + .../config/llm_validator_resilience_test.go | 122 +++++ .../pkg/utils/llm_resilience_test.go | 236 ++++++++++ .../pkg/utils/llm_transformer.go | 33 ++ gateway/it/features/backend-timeout.feature | 6 +- .../it/features/llm-backend-timeout.feature | 225 +++++++++ .../features/upstream-connect-timeout.feature | 6 +- gateway/it/steps_timeouts.go | 22 + gateway/it/suite_test.go | 1 + .../api/v1alpha1/llmprovider_types.go | 5 + .../api/v1alpha1/llmproxy_types.go | 5 + .../api/v1alpha1/zz_generated.deepcopy.go | 10 + ...ay.api-platform.wso2.com_llmproviders.yaml | 15 + ...eway.api-platform.wso2.com_llmproxies.yaml | 15 + ...ay.api-platform.wso2.com_llmproviders.yaml | 15 + ...eway.api-platform.wso2.com_llmproxies.yaml | 15 + 19 files changed, 977 insertions(+), 225 deletions(-) create mode 100644 gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go create mode 100644 gateway/gateway-controller/pkg/utils/llm_resilience_test.go create mode 100644 gateway/it/features/llm-backend-timeout.feature diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index 9dbf7d5e5c..d38c74cd5c 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -5818,6 +5818,13 @@ components: enum: [deployed, undeployed] default: deployed example: deployed + resilience: + $ref: '#/components/schemas/Resilience' + description: > + API-level backend/route timeout configuration. Applies to all routes generated + for this LLM Provider (the routes that forward traffic upstream). Supported at the + API level only - LLM routes are synthesized by the gateway, so there is no + operation-level override. UpstreamAuth: type: object @@ -6108,6 +6115,13 @@ components: enum: [deployed, undeployed] default: deployed example: deployed + resilience: + $ref: '#/components/schemas/Resilience' + description: > + API-level backend/route timeout configuration. Applies to all routes generated + for this LLM Proxy (the routes that forward traffic upstream). Supported at the + API level only - LLM routes are synthesized by the gateway, so there is no + operation-level override. SecretConfigurationRequest: type: object diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index 375822d2be..678131e055 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -790,6 +790,9 @@ type LLMProviderConfigData struct { // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Policies *[]LLMPolicy `json:"policies,omitempty" yaml:"policies,omitempty"` + // Resilience Backend/route timeout configuration. Maps to Envoy RouteAction timeouts. Can be set at the API level (applies to all routes) and/or the operation level (applies to that operation's route). When set at both levels, the operation-level value takes precedence. When unset, the gateway's global route timeout defaults apply. + Resilience *Resilience `json:"resilience,omitempty" yaml:"resilience,omitempty"` + // Template Template name to use for this LLM Provider Template string `json:"template" yaml:"template"` Upstream LLMProviderConfigData_Upstream `json:"upstream" yaml:"upstream"` @@ -963,6 +966,9 @@ type LLMProxyConfigData struct { Policies *[]LLMPolicy `json:"policies,omitempty" yaml:"policies,omitempty"` Provider LLMProxyProvider `json:"provider" yaml:"provider"` + // Resilience Backend/route timeout configuration. Maps to Envoy RouteAction timeouts. Can be set at the API level (applies to all routes) and/or the operation level (applies to that operation's route). When set at both levels, the operation-level value takes precedence. When unset, the gateway's global route timeout defaults apply. + Resilience *Resilience `json:"resilience,omitempty" yaml:"resilience,omitempty"` + // Version Semantic version of the LLM proxy Version string `json:"version" yaml:"version"` @@ -5823,225 +5829,225 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/gateway/gateway-controller/pkg/config/api_validator.go b/gateway/gateway-controller/pkg/config/api_validator.go index 34ba29a49f..41e4b3fd2a 100644 --- a/gateway/gateway-controller/pkg/config/api_validator.go +++ b/gateway/gateway-controller/pkg/config/api_validator.go @@ -450,6 +450,11 @@ func (v *APIValidator) validateRestData(spec *api.APIConfigData) []ValidationErr // malformed values are rejected. fieldPrefix is the path to the block (e.g. // "spec.resilience" or "spec.operations[2].resilience"). func (v *APIValidator) validateResilience(fieldPrefix string, r *api.Resilience) []ValidationError { + return validateResilienceTimeouts(fieldPrefix, r) +} + +// validateResilienceTimeouts validates the timeout fields of a resilience block. +func validateResilienceTimeouts(fieldPrefix string, r *api.Resilience) []ValidationError { var errors []ValidationError if r == nil { return errors diff --git a/gateway/gateway-controller/pkg/config/llm_validator.go b/gateway/gateway-controller/pkg/config/llm_validator.go index 9a1ca3cf08..67c82965ff 100644 --- a/gateway/gateway-controller/pkg/config/llm_validator.go +++ b/gateway/gateway-controller/pkg/config/llm_validator.go @@ -387,6 +387,10 @@ func (v *LLMValidator) validateProviderSpec(spec *api.LLMProviderConfigData) []V // The deprecated `policies` list must not coexist with the new policy lists errors = append(errors, v.validatePolicyListExclusivity(spec.GlobalPolicies, spec.OperationPolicies, spec.Policies)...) + // Validate API-level resilience (timeout / idleTimeout). LLM kinds support resilience at + // the API level only. + errors = append(errors, validateResilienceTimeouts("spec.resilience", spec.Resilience)...) + return errors } @@ -594,6 +598,10 @@ func (v *LLMValidator) validateProxyData(spec *api.LLMProxyConfigData) []Validat // The deprecated `policies` list must not coexist with the new policy lists errors = append(errors, v.validatePolicyListExclusivity(spec.GlobalPolicies, spec.OperationPolicies, spec.Policies)...) + // Validate API-level resilience (timeout / idleTimeout). LLM kinds support resilience at + // the API level only. + errors = append(errors, validateResilienceTimeouts("spec.resilience", spec.Resilience)...) + return errors } diff --git a/gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go b/gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go new file mode 100644 index 0000000000..7523bfe48c --- /dev/null +++ b/gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + api "github.com/wso2/api-platform/gateway/gateway-controller/pkg/api/management" +) + +func validProviderWithResilience(r *api.Resilience) api.LLMProviderConfiguration { + return api.LLMProviderConfiguration{ + ApiVersion: "gateway.api-platform.wso2.com/v1", + Kind: api.LLMProviderConfigurationKindLlmProvider, + Metadata: api.Metadata{Name: "openai"}, + Spec: api.LLMProviderConfigData{ + DisplayName: "my-provider", + Version: "v1.0", + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://api.openai.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + Resilience: r, + }, + } +} + +func validProxyWithResilience(r *api.Resilience) api.LLMProxyConfiguration { + return api.LLMProxyConfiguration{ + ApiVersion: api.LLMProxyConfigurationApiVersionGatewayApiPlatformWso2Comv1, + Kind: api.LLMProxyConfigurationKindLlmProxy, + Metadata: api.Metadata{Name: "openai-proxy"}, + Spec: api.LLMProxyConfigData{ + DisplayName: "my-proxy", + Version: "v1.0", + Provider: api.LLMProxyProvider{Id: "openai"}, + Resilience: r, + }, + } +} + +func TestValidateLLMProvider_Resilience(t *testing.T) { + validator := NewLLMValidator() + + t.Run("valid timeout and idleTimeout", func(t *testing.T) { + errs := validator.Validate(validProviderWithResilience(&api.Resilience{ + Timeout: stringPtr("30s"), + IdleTimeout: stringPtr("0s"), + })) + assert.Empty(t, errs) + }) + + t.Run("nil resilience is fine", func(t *testing.T) { + errs := validator.Validate(validProviderWithResilience(nil)) + assert.Empty(t, errs) + }) + + t.Run("malformed timeout is rejected", func(t *testing.T) { + errs := validator.Validate(validProviderWithResilience(&api.Resilience{Timeout: stringPtr("30")})) + assertHasFieldError(t, errs, "spec.resilience.timeout") + }) + + t.Run("negative timeout is rejected", func(t *testing.T) { + errs := validator.Validate(validProviderWithResilience(&api.Resilience{Timeout: stringPtr("-5s")})) + assertHasFieldError(t, errs, "spec.resilience.timeout") + }) + + t.Run("malformed idleTimeout is rejected", func(t *testing.T) { + errs := validator.Validate(validProviderWithResilience(&api.Resilience{IdleTimeout: stringPtr("abc")})) + assertHasFieldError(t, errs, "spec.resilience.idleTimeout") + }) +} + +func TestValidateLLMProxy_Resilience(t *testing.T) { + validator := NewLLMValidator() + + t.Run("valid timeout", func(t *testing.T) { + errs := validator.Validate(validProxyWithResilience(&api.Resilience{Timeout: stringPtr("75s")})) + assert.Empty(t, errs) + }) + + t.Run("nil resilience is fine", func(t *testing.T) { + errs := validator.Validate(validProxyWithResilience(nil)) + assert.Empty(t, errs) + }) + + t.Run("malformed timeout is rejected", func(t *testing.T) { + errs := validator.Validate(validProxyWithResilience(&api.Resilience{Timeout: stringPtr("fast")})) + assertHasFieldError(t, errs, "spec.resilience.timeout") + }) + + t.Run("negative idleTimeout is rejected", func(t *testing.T) { + errs := validator.Validate(validProxyWithResilience(&api.Resilience{IdleTimeout: stringPtr("-1s")})) + assertHasFieldError(t, errs, "spec.resilience.idleTimeout") + }) +} + +func assertHasFieldError(t *testing.T, errs []ValidationError, field string) { + t.Helper() + for _, e := range errs { + if e.Field == field { + return + } + } + t.Fatalf("expected a validation error on field %q, got %+v", field, errs) +} diff --git a/gateway/gateway-controller/pkg/utils/llm_resilience_test.go b/gateway/gateway-controller/pkg/utils/llm_resilience_test.go new file mode 100644 index 0000000000..38cb518f17 --- /dev/null +++ b/gateway/gateway-controller/pkg/utils/llm_resilience_test.go @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package utils + +import ( + "io" + "log/slog" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + api "github.com/wso2/api-platform/gateway/gateway-controller/pkg/api/management" + "github.com/wso2/api-platform/gateway/gateway-controller/pkg/config" + "github.com/wso2/api-platform/gateway/gateway-controller/pkg/models" + "github.com/wso2/api-platform/gateway/gateway-controller/pkg/storage" +) + +// These tests pin the LLM resilience route-mapping rule: API-level resilience is attached to every +// traffic-forwarding route and never to the access-control deny routes. + +// In allow_all mode, the catch-all (and any policy-derived routes) carry the resilience block while +// the exception/deny routes (which only return a 404) are left untouched. +func TestTransformProvider_AllowAll_ResilienceSkipsDenyRoutes(t *testing.T) { + transformer, _ := setupTestTransformer(t) + + exceptions := []api.RouteException{ + {Path: "/admin", Methods: []api.RouteExceptionMethods{api.RouteExceptionMethodsGET, api.RouteExceptionMethodsPOST}}, + } + timeout := "30s" + provider := &api.LLMProviderConfiguration{ + ApiVersion: "gateway.api-platform.wso2.com/v1", + Kind: "LlmProvider", + Metadata: api.Metadata{Name: "openai-provider"}, + Spec: api.LLMProviderConfigData{ + DisplayName: "test", + Version: "v1.0", + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://api.example.com")}, + AccessControl: api.LLMAccessControl{ + Mode: api.AllowAll, + Exceptions: &exceptions, + }, + Resilience: &api.Resilience{Timeout: &timeout}, + }, + } + + result, err := transformer.Transform(provider, &api.RestAPI{}) + require.NoError(t, err) + + sawCatchAll := false + sawDeny := false + for _, op := range result.Spec.Operations { + if op.Path == "/admin" { // the deny/exception routes + sawDeny = true + assert.Nil(t, op.Resilience, "deny route %s %s must not carry resilience", op.Method, op.Path) + continue + } + // catch-all (traffic-forwarding) routes + sawCatchAll = true + require.NotNil(t, op.Resilience, "traffic route %s %s should carry resilience", op.Method, op.Path) + require.NotNil(t, op.Resilience.Timeout) + assert.Equal(t, "30s", *op.Resilience.Timeout) + } + assert.True(t, sawCatchAll, "expected at least one traffic-forwarding route") + assert.True(t, sawDeny, "expected at least one deny route") +} + +// In deny_all mode every created route is an allow-listed forwarding route, so all of them carry the +// resilience block. +func TestTransformProvider_DenyAll_ResilienceOnAllRoutes(t *testing.T) { + transformer, _ := setupTestTransformer(t) + + exceptions := []api.RouteException{ + {Path: "/chat/completions", Methods: []api.RouteExceptionMethods{api.RouteExceptionMethodsPOST}}, + {Path: "/models", Methods: []api.RouteExceptionMethods{api.RouteExceptionMethodsGET}}, + } + timeout := "45s" + idle := "0s" + provider := &api.LLMProviderConfiguration{ + ApiVersion: "gateway.api-platform.wso2.com/v1", + Kind: "LlmProvider", + Metadata: api.Metadata{Name: "openai-provider"}, + Spec: api.LLMProviderConfigData{ + DisplayName: "test", + Version: "v1.0", + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://api.example.com")}, + AccessControl: api.LLMAccessControl{ + Mode: api.DenyAll, + Exceptions: &exceptions, + }, + Resilience: &api.Resilience{Timeout: &timeout, IdleTimeout: &idle}, + }, + } + + result, err := transformer.Transform(provider, &api.RestAPI{}) + require.NoError(t, err) + require.NotEmpty(t, result.Spec.Operations) + + for _, op := range result.Spec.Operations { + require.NotNil(t, op.Resilience, "route %s %s should carry resilience", op.Method, op.Path) + require.NotNil(t, op.Resilience.Timeout) + assert.Equal(t, "45s", *op.Resilience.Timeout) + require.NotNil(t, op.Resilience.IdleTimeout) + assert.Equal(t, "0s", *op.Resilience.IdleTimeout) + } +} + +// With no resilience block on the source provider, no operation gets a resilience block (unchanged +// behavior — routes fall back to the gateway's global default timeout). +func TestTransformProvider_NoResilience_NotAttached(t *testing.T) { + transformer, _ := setupTestTransformer(t) + + provider := &api.LLMProviderConfiguration{ + ApiVersion: "gateway.api-platform.wso2.com/v1", + Kind: "LlmProvider", + Metadata: api.Metadata{Name: "openai-provider"}, + Spec: api.LLMProviderConfigData{ + DisplayName: "test", + Version: "v1.0", + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://api.example.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + }, + } + + result, err := transformer.Transform(provider, &api.RestAPI{}) + require.NoError(t, err) + require.NotEmpty(t, result.Spec.Operations) + + for _, op := range result.Spec.Operations { + assert.Nil(t, op.Resilience, "route %s %s should not carry resilience", op.Method, op.Path) + } +} + +// A proxy is always allow-all with no access control, so every generated route carries the +// resilience block. +func TestTransformProxy_ResilienceOnAllRoutes(t *testing.T) { + store := storage.NewConfigStore() + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + db := newTestSQLiteStorage(t, logger) + + template := &models.StoredLLMProviderTemplate{ + UUID: "0000-db-template-id-0000-000000000001", + Configuration: api.LLMProviderTemplate{ + ApiVersion: api.LLMProviderTemplateApiVersionGatewayApiPlatformWso2Comv1, + Kind: api.LLMProviderTemplateKindLlmProviderTemplate, + Metadata: api.Metadata{Name: "openai"}, + Spec: api.LLMProviderTemplateData{DisplayName: "openai"}, + }, + } + require.NoError(t, db.SaveLLMProviderTemplate(template)) + + now := time.Now() + provider := &models.StoredConfig{ + UUID: "0000-db-provider-id-0000-000000000000", + Kind: string(api.LLMProviderConfigurationKindLlmProvider), + Handle: "db-provider", + DisplayName: "db-provider", + Version: "v1.0", + Configuration: api.RestAPI{ + ApiVersion: api.RestAPIApiVersionGatewayApiPlatformWso2Comv1, + Kind: api.RestAPIKindRestApi, + Metadata: api.Metadata{Name: "db-provider"}, + Spec: api.APIConfigData{ + DisplayName: "db-provider", + Version: "v1.0", + Context: "/db-provider", + Upstream: struct { + Main api.Upstream `json:"main" yaml:"main"` + Sandbox *api.Upstream `json:"sandbox,omitempty" yaml:"sandbox,omitempty"` + }{Main: api.Upstream{Url: stringPtr("https://api.openai.com")}}, + }, + }, + SourceConfiguration: api.LLMProviderConfiguration{ + ApiVersion: api.LLMProviderConfigurationApiVersionGatewayApiPlatformWso2Comv1, + Kind: api.LLMProviderConfigurationKindLlmProvider, + Metadata: api.Metadata{Name: "db-provider"}, + Spec: api.LLMProviderConfigData{ + DisplayName: "db-provider", + Version: "v1.0", + Context: stringPtr("/db-provider"), + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://api.openai.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + }, + }, + DesiredState: models.StateDeployed, + CreatedAt: now, + UpdatedAt: now, + } + require.NoError(t, db.SaveConfig(provider)) + + routerConfig := &config.RouterConfig{ListenerPort: 8080} + transformer := NewLLMProviderTransformer(store, db, routerConfig, newTestPolicyVersionResolver()) + + timeout := "75s" + proxy := &api.LLMProxyConfiguration{ + ApiVersion: api.LLMProxyConfigurationApiVersionGatewayApiPlatformWso2Comv1, + Kind: api.LLMProxyConfigurationKindLlmProxy, + Metadata: api.Metadata{Name: "db-proxy"}, + Spec: api.LLMProxyConfigData{ + DisplayName: "db-proxy", + Version: "v1.0", + Provider: api.LLMProxyProvider{Id: "db-provider"}, + Resilience: &api.Resilience{Timeout: &timeout}, + }, + } + + result, err := transformer.Transform(proxy, &api.RestAPI{}) + require.NoError(t, err) + require.NotEmpty(t, result.Spec.Operations) + + for _, op := range result.Spec.Operations { + require.NotNil(t, op.Resilience, "proxy route %s %s should carry resilience", op.Method, op.Path) + require.NotNil(t, op.Resilience.Timeout) + assert.Equal(t, "75s", *op.Resilience.Timeout) + } +} diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer.go b/gateway/gateway-controller/pkg/utils/llm_transformer.go index 0146d2ad64..d4e1409062 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer.go @@ -295,6 +295,9 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } } } + // A proxy is always allow-all with no access control, so there are no deny routes: + // attach API-level resilience to all generated routes. + applyResilienceToTrafficRoutes(ops, proxy.Spec.Resilience, nil) spec.Operations = ops // Global (api-level) policies: route into the derived RestAPI's spec.Policies so they are @@ -388,6 +391,12 @@ func (t *LLMProviderTransformer) transformProvider(provider *api.LLMProviderConf var ops []api.Operation + // denyOpKeys tracks the routes created purely to deny traffic (the access-control + // exception routes, which carry the 404 respond policy). API-level resilience is NOT + // attached to these (they never reach an upstream). Populated in allow_all mode; empty + // in deny_all (where every created route is an allowed/forwarding route). + denyOpKeys := make(map[pathMethodKey]bool) + switch mode { case api.AllowAll: var denyPolicyVersion string @@ -427,6 +436,7 @@ func (t *LLMProviderTransformer) transformProvider(provider *api.LLMProviderConf for _, method := range methods { key := pathMethodKey{path: ex.Path, method: method} deniedPathMethods[key] = true + denyOpKeys[key] = true // Check if operation exists if _, exists := operationRegistry[key]; !exists { @@ -634,6 +644,8 @@ func (t *LLMProviderTransformer) transformProvider(provider *api.LLMProviderConf } } } + // Attach API-level resilience to every traffic-forwarding route, skipping the deny routes. + applyResilienceToTrafficRoutes(ops, provider.Spec.Resilience, denyOpKeys) spec.Operations = ops // Global (api-level) policies: route into the derived RestAPI's spec.Policies so they are @@ -658,6 +670,27 @@ func (t *LLMProviderTransformer) transformProvider(provider *api.LLMProviderConf return output, nil } +// applyResilienceToTrafficRoutes attaches the API-level resilience block to every operation that +// forwards traffic upstream, skipping access-control deny routes (which only return a canned 404 +// and never reach an upstream, so a timeout on them is meaningless). denyKeys identifies the deny +// routes by path+method; it is empty for deny_all and proxy (no deny routes exist there), so in +// those cases the block is applied to all routes. A nil resilience block is a no-op. +// +// Resilience is attached at the operation level (not the derived API level) on purpose: that keeps +// the deny routes on the gateway's global default timeout instead of inheriting the API-level value +// via the translator's per-field fallback. +func applyResilienceToTrafficRoutes(ops []api.Operation, resilience *api.Resilience, denyKeys map[pathMethodKey]bool) { + if resilience == nil { + return + } + for i := range ops { + if denyKeys[pathMethodKey{path: ops[i].Path, method: string(ops[i].Method)}] { + continue + } + ops[i].Resilience = resilience + } +} + // GetUpstreamAuthApikeyPolicyParams renders the policy params with given header and value func GetUpstreamAuthApikeyPolicyParams(header, value string) (map[string]interface{}, error) { rendered := fmt.Sprintf(constants.UPSTREAM_AUTH_APIKEY_POLICY_PARAMS, header, value) diff --git a/gateway/it/features/backend-timeout.feature b/gateway/it/features/backend-timeout.feature index 031a28ca86..d701f21f4d 100644 --- a/gateway/it/features/backend-timeout.feature +++ b/gateway/it/features/backend-timeout.feature @@ -36,7 +36,7 @@ Feature: Backend route timeouts via the resilience block Given I authenticate using basic auth as "admin" When I deploy this API configuration: """ - apiVersion: gateway.api-platform.wso2.com/v1alpha1 + apiVersion: gateway.api-platform.wso2.com/v1 kind: RestApi metadata: name: backend-timeout-api-v1.0 @@ -72,7 +72,7 @@ Feature: Backend route timeouts via the resilience block Given I authenticate using basic auth as "admin" When I deploy this API configuration: """ - apiVersion: gateway.api-platform.wso2.com/v1alpha1 + apiVersion: gateway.api-platform.wso2.com/v1 kind: RestApi metadata: name: backend-timeout-override-v1.0 @@ -109,7 +109,7 @@ Feature: Backend route timeouts via the resilience block Given I authenticate using basic auth as "admin" When I deploy this API configuration: """ - apiVersion: gateway.api-platform.wso2.com/v1alpha1 + apiVersion: gateway.api-platform.wso2.com/v1 kind: RestApi metadata: name: backend-timeout-default-v1.0 diff --git a/gateway/it/features/llm-backend-timeout.feature b/gateway/it/features/llm-backend-timeout.feature new file mode 100644 index 0000000000..98b72aba88 --- /dev/null +++ b/gateway/it/features/llm-backend-timeout.feature @@ -0,0 +1,225 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# -------------------------------------------------------------------- + +@llm @backend-timeout @resilience +Feature: Backend route timeouts for LLM providers and proxies via the resilience block + As an API developer + I want to configure the route timeout via an API-level resilience block on LLM providers and proxies + So that requests to slow LLM backends are terminated by the gateway within the configured time + + # LLM kinds support resilience at the API level only + + Background: + Given the gateway services are running + + # allow_all creates a catch-all route for all traffic; the API-level resilience.timeout (2s) is + # shorter than the backend delay (5s), so the gateway times the route out with 504 at ~2s. + Scenario: LLM provider API-level resilience timeout terminates a slow backend + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1 + kind: LlmProvider + metadata: + name: llm-timeout-provider + spec: + displayName: LLM Timeout Provider + version: v1.0 + template: openai + context: /llm-timeout + upstream: + url: http://echo-backend:80 + accessControl: + mode: allow_all + resilience: + timeout: 2s + """ + Then the response status code should be 201 + And I wait for the endpoint "http://localhost:8080/llm-timeout/get" to be ready + And I record the current time as "request_start" + When I send a GET request to "http://localhost:8080/llm-timeout/delay/5" + Then the response status code should be 504 + And the request should have taken at least "2" seconds since "request_start" + Given I authenticate using basic auth as "admin" + When I delete the LLM provider "llm-timeout-provider" + Then the response should be successful + + # deny_all creates routes only for the allow-listed exception paths; the resilience block must be + # attached to those (forwarding) routes too. The allow-listed /delay/5 route times out at ~2s. + Scenario: LLM provider deny_all attaches the timeout to allow-listed routes + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1 + kind: LlmProvider + metadata: + name: llm-timeout-deny-provider + spec: + displayName: LLM Timeout Deny Provider + version: v1.0 + template: openai + context: /llm-timeout-deny + upstream: + url: http://echo-backend:80 + accessControl: + mode: deny_all + exceptions: + - path: /delay/5 + methods: [GET] + resilience: + timeout: 2s + """ + Then the response status code should be 201 + And I record the current time as "request_start" + When I send a GET request to "http://localhost:8080/llm-timeout-deny/delay/5" + Then the response status code should be 504 + And the request should have taken at least "2" seconds since "request_start" + Given I authenticate using basic auth as "admin" + When I delete the LLM provider "llm-timeout-deny-provider" + Then the response should be successful + + # Without a resilience block the global route timeout default (60s) applies, so a backend that + # responds within it succeeds normally. + Scenario: LLM provider without a resilience block falls back to the global default and succeeds + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1 + kind: LlmProvider + metadata: + name: llm-timeout-default-provider + spec: + displayName: LLM Timeout Default Provider + version: v1.0 + template: openai + context: /llm-timeout-default + upstream: + url: http://echo-backend:80 + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + And I wait for the endpoint "http://localhost:8080/llm-timeout-default/get" to be ready + When I send a GET request to "http://localhost:8080/llm-timeout-default/delay/2" + Then the response status code should be 200 + Given I authenticate using basic auth as "admin" + When I delete the LLM provider "llm-timeout-default-provider" + Then the response should be successful + + # A proxy is always allow-all (catch-all). The proxy's own API-level resilience.timeout (2s) + # bounds the whole proxied call (client -> proxy route -> loopback -> provider route -> backend). + # The backing provider is left on the global default, so the 504 is attributable to the proxy. + Scenario: LLM proxy API-level resilience timeout terminates a slow backend + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1 + kind: LlmProvider + metadata: + name: llm-timeout-proxy-provider + spec: + displayName: LLM Timeout Proxy Provider + version: v1.0 + template: openai + context: /llm-timeout-backing + upstream: + url: http://echo-backend:80 + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1 + kind: LlmProxy + metadata: + name: llm-timeout-proxy + spec: + displayName: LLM Timeout Proxy + version: v1.0 + context: /llm-timeout-proxy + provider: + id: llm-timeout-proxy-provider + resilience: + timeout: 2s + """ + Then the response status code should be 201 + And I wait for the endpoint "http://localhost:8080/llm-timeout-proxy/get" to be ready + And I record the current time as "request_start" + When I send a GET request to "http://localhost:8080/llm-timeout-proxy/delay/5" + Then the response status code should be 504 + And the request should have taken at least "2" seconds since "request_start" + Given I authenticate using basic auth as "admin" + When I send a DELETE request to the "gateway-controller" service at "/llm-proxies/llm-timeout-proxy" + Then the response should be successful + Given I authenticate using basic auth as "admin" + When I delete the LLM provider "llm-timeout-proxy-provider" + Then the response should be successful + + # Both the proxy and its backing provider set their own resilience.timeout. The request traverses + # both routes (proxy 6s -> loopback -> provider 2s -> backend /delay/10), so the shorter inner + # provider timeout (2s) must fire first and bound the whole call. The upper-bound assertion proves + # the 2s provider timeout won, not the 6s proxy timeout. + Scenario: LLM proxy and provider each set resilience and the shorter (provider) timeout wins + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1 + kind: LlmProvider + metadata: + name: llm-timeout-both-provider + spec: + displayName: LLM Timeout Both Provider + version: v1.0 + template: openai + context: /llm-timeout-both-backing + upstream: + url: http://echo-backend:80 + accessControl: + mode: allow_all + resilience: + timeout: 2s + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1 + kind: LlmProxy + metadata: + name: llm-timeout-both-proxy + spec: + displayName: LLM Timeout Both Proxy + version: v1.0 + context: /llm-timeout-both + provider: + id: llm-timeout-both-provider + resilience: + timeout: 6s + """ + Then the response status code should be 201 + And I wait for the endpoint "http://localhost:8080/llm-timeout-both/get" to be ready + And I record the current time as "request_start" + When I send a GET request to "http://localhost:8080/llm-timeout-both/delay/10" + Then the response status code should be 504 + And the request should have taken at least "2" seconds since "request_start" + And the request should have taken at most "4" seconds since "request_start" + Given I authenticate using basic auth as "admin" + When I send a DELETE request to the "gateway-controller" service at "/llm-proxies/llm-timeout-both-proxy" + Then the response should be successful + Given I authenticate using basic auth as "admin" + When I delete the LLM provider "llm-timeout-both-provider" + Then the response should be successful diff --git a/gateway/it/features/upstream-connect-timeout.feature b/gateway/it/features/upstream-connect-timeout.feature index f2ae26afeb..f0374b89cd 100644 --- a/gateway/it/features/upstream-connect-timeout.feature +++ b/gateway/it/features/upstream-connect-timeout.feature @@ -47,7 +47,7 @@ Feature: Timeouts timeout: connect: 6000ms upstreams: - - url: http://192.0.2.1:80 + - url: 10.255.255.1:8080 upstream: main: ref: my-timeout-upstream @@ -82,7 +82,7 @@ Feature: Timeouts upstreamDefinitions: - name: my-timeout-upstream-global upstreams: - - url: http://192.0.2.1:80 + - url: http://10.255.255.1:8080 upstream: main: ref: my-timeout-upstream-global @@ -108,7 +108,7 @@ Feature: Timeouts Given I authenticate using basic auth as "admin" When I deploy this API configuration: """ - apiVersion: gateway.api-platform.wso2.com/v1alpha1 + apiVersion: gateway.api-platform.wso2.com/v1 kind: RestApi metadata: name: headers-timeout-api-v1.0 diff --git a/gateway/it/steps_timeouts.go b/gateway/it/steps_timeouts.go index 744353fef6..f8bea39830 100644 --- a/gateway/it/steps_timeouts.go +++ b/gateway/it/steps_timeouts.go @@ -64,6 +64,28 @@ func RegisterTimeoutSteps(ctx *godog.ScenarioContext, state *TestState) { return nil }) + ctx.Step(`^the request should have taken at most "(\d+)" seconds since "([^"]*)"$`, func(expectedSecondsStr, key string) error { + expectedSeconds, err := strconv.Atoi(expectedSecondsStr) + if err != nil { + return fmt.Errorf("expected seconds must be a number, got: %s", expectedSecondsStr) + } + val, ok := state.GetContextValue(key) + if !ok { + return fmt.Errorf("no start time recorded; record the current time as \"request_start\" before sending the request") + } + start, ok := val.(time.Time) + if !ok { + return fmt.Errorf("no start time recorded; record the current time as %q before sending the request", key) + } + elapsed := time.Since(start) + maxElapsed := time.Duration(expectedSeconds+elapsedTimeToleranceSeconds) * time.Second + if elapsed > maxElapsed { + return fmt.Errorf("request should have taken at most %d seconds (with %ds tolerance), but elapsed time was %s", + expectedSeconds, elapsedTimeToleranceSeconds, elapsed.Round(time.Millisecond)) + } + return nil + }) + // Opens a raw TCP connection and sends a request line plus one header, but never // terminates the header block (no final blank line). This forces the HCM // request_headers_timeout to fire. The connection blocks until the gateway responds diff --git a/gateway/it/suite_test.go b/gateway/it/suite_test.go index f8c017122c..d994bd8e6e 100644 --- a/gateway/it/suite_test.go +++ b/gateway/it/suite_test.go @@ -145,6 +145,7 @@ func getFeaturePaths() []string { "features/template-functions.feature", "features/upstream-connect-timeout.feature", "features/backend-timeout.feature", + "features/llm-backend-timeout.feature", // Runs late: it restarts the gateway-controller (reject/reconnect scenario), so keep it // after features that assume an uninterrupted controller. Verifies the DP->CP artifact push. "features/dp-to-cp.feature", diff --git a/kubernetes/gateway-operator/api/v1alpha1/llmprovider_types.go b/kubernetes/gateway-operator/api/v1alpha1/llmprovider_types.go index 5d2fae5583..9535b86216 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/llmprovider_types.go +++ b/kubernetes/gateway-operator/api/v1alpha1/llmprovider_types.go @@ -128,6 +128,11 @@ type LLMProviderConfigData struct { // Policies is the list of policies applied to this LLM provider. // +optional Policies []LLMPolicy `json:"policies,omitempty"` + + // Resilience configures API-level backend/route timeouts applied to all routes + // generated for this LLM provider. Supported at the API level only. + // +optional + Resilience *Resilience `json:"resilience,omitempty"` } // LLMPolicyPath defines a path/methods combination together with policy diff --git a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go index 046f492af5..62aac873c9 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go +++ b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go @@ -64,6 +64,11 @@ type LLMProxyConfigData struct { // Policies is the list of policies applied to this LLM proxy. // +optional Policies []LLMPolicy `json:"policies,omitempty"` + + // Resilience configures API-level backend/route timeouts applied to all routes + // generated for this LLM proxy. Supported at the API level only. + // +optional + Resilience *Resilience `json:"resilience,omitempty"` } //+kubebuilder:object:root=true diff --git a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go index 14c78bcddc..69e6912d3f 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -802,6 +802,11 @@ func (in *LLMProviderConfigData) DeepCopyInto(out *LLMProviderConfigData) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Resilience != nil { + in, out := &in.Resilience, &out.Resilience + *out = new(Resilience) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LLMProviderConfigData. @@ -992,6 +997,11 @@ func (in *LLMProxyConfigData) DeepCopyInto(out *LLMProxyConfigData) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Resilience != nil { + in, out := &in.Resilience, &out.Resilience + *out = new(Resilience) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LLMProxyConfigData. diff --git a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproviders.yaml b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproviders.yaml index 8d48f6cbe8..78f1879087 100644 --- a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproviders.yaml +++ b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproviders.yaml @@ -153,6 +153,21 @@ spec: - version type: object type: array + resilience: + description: |- + Resilience configures API-level backend/route timeouts applied to all routes + generated for this LLM provider. Supported at the API level only. + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object template: description: Template is the LlmProviderTemplate name to apply. type: string diff --git a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..81da02ebd1 100644 --- a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml @@ -180,6 +180,21 @@ spec: required: - id type: object + resilience: + description: |- + Resilience configures API-level backend/route timeouts applied to all routes + generated for this LLM proxy. Supported at the API level only. + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object version: description: Version is the semantic version of the LLM proxy. pattern: ^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ diff --git a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproviders.yaml b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproviders.yaml index 8d48f6cbe8..78f1879087 100644 --- a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproviders.yaml +++ b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproviders.yaml @@ -153,6 +153,21 @@ spec: - version type: object type: array + resilience: + description: |- + Resilience configures API-level backend/route timeouts applied to all routes + generated for this LLM provider. Supported at the API level only. + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object template: description: Template is the LlmProviderTemplate name to apply. type: string diff --git a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..81da02ebd1 100644 --- a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml @@ -180,6 +180,21 @@ spec: required: - id type: object + resilience: + description: |- + Resilience configures API-level backend/route timeouts applied to all routes + generated for this LLM proxy. Supported at the API level only. + properties: + idleTimeout: + description: IdleTimeout Per-route stream idle timeout. "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + timeout: + description: Timeout Maximum time for the entire route (request + to upstream response). "0s" disables. + pattern: ^\d+(\.\d+)?(ms|s|m|h)$ + type: string + type: object version: description: Version is the semantic version of the LLM proxy. pattern: ^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ From 04542e011ec2b4cc1df93ad0b26ea6698591e125 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Fri, 26 Jun 2026 07:00:18 +0530 Subject: [PATCH 7/8] Fix test failures in connect timeout --- gateway/it/features/llm-backend-timeout.feature | 3 +++ gateway/it/features/upstream-connect-timeout.feature | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gateway/it/features/llm-backend-timeout.feature b/gateway/it/features/llm-backend-timeout.feature index 98b72aba88..addc30347b 100644 --- a/gateway/it/features/llm-backend-timeout.feature +++ b/gateway/it/features/llm-backend-timeout.feature @@ -78,12 +78,15 @@ Feature: Backend route timeouts for LLM providers and proxies via the resilience accessControl: mode: deny_all exceptions: + - path: /get + methods: [GET] - path: /delay/5 methods: [GET] resilience: timeout: 2s """ Then the response status code should be 201 + And I wait for the endpoint "http://localhost:8080/llm-timeout-deny/get" to be ready And I record the current time as "request_start" When I send a GET request to "http://localhost:8080/llm-timeout-deny/delay/5" Then the response status code should be 504 diff --git a/gateway/it/features/upstream-connect-timeout.feature b/gateway/it/features/upstream-connect-timeout.feature index f0374b89cd..b20d5b819f 100644 --- a/gateway/it/features/upstream-connect-timeout.feature +++ b/gateway/it/features/upstream-connect-timeout.feature @@ -47,7 +47,7 @@ Feature: Timeouts timeout: connect: 6000ms upstreams: - - url: 10.255.255.1:8080 + - url: http://10.255.255.1:8080 upstream: main: ref: my-timeout-upstream @@ -58,6 +58,7 @@ Feature: Timeouts Then the response should be successful And the response should be valid JSON And the JSON response field "status" should be "success" + And I wait for policy snapshot sync And I record the current time as "request_start" When I send a GET request to "http://localhost:8080/timeout-api/v1.0/" Then the response status code should be 503 @@ -93,6 +94,7 @@ Feature: Timeouts Then the response should be successful And the response should be valid JSON And the JSON response field "status" should be "success" + And I wait for policy snapshot sync And I record the current time as "request_start" When I send a GET request to "http://localhost:8080/timeout-global/v1.0/" Then the response status code should be 503 From f8b3ebb6fde144310d57c4a92c60d938ad2c1605 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Fri, 26 Jun 2026 10:22:39 +0530 Subject: [PATCH 8/8] Address coderabbit review comments --- gateway/configs/config-template.toml | 2 +- .../pkg/config/api_validator.go | 15 +++-- .../pkg/config/api_validator_test.go | 6 ++ .../config/llm_validator_resilience_test.go | 10 ++++ .../pkg/constants/constants.go | 11 ++++ .../pkg/xds/resilience_duration_test.go | 58 +++++++++++++++++++ .../gateway-controller/pkg/xds/translator.go | 17 +++--- .../features/upstream-connect-timeout.feature | 4 +- 8 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 gateway/gateway-controller/pkg/xds/resilience_duration_test.go diff --git a/gateway/configs/config-template.toml b/gateway/configs/config-template.toml index 94aa4be649..2291942f24 100644 --- a/gateway/configs/config-template.toml +++ b/gateway/configs/config-template.toml @@ -213,7 +213,7 @@ server_header_transformation = "OVERWRITE" server_header_value = "WSO2 API Platform" # HTTP Connection Manager (downstream) timeouts -# Except for idle_timeout, a value of 0 disables all other timeout settings. +# A value of zero disables any of these timeout settings. [router.http_listener.timeouts] # Max duration for the entire downstream request request_timeout = "0s" diff --git a/gateway/gateway-controller/pkg/config/api_validator.go b/gateway/gateway-controller/pkg/config/api_validator.go index 41e4b3fd2a..daefcfee0a 100644 --- a/gateway/gateway-controller/pkg/config/api_validator.go +++ b/gateway/gateway-controller/pkg/config/api_validator.go @@ -26,6 +26,7 @@ import ( "time" api "github.com/wso2/api-platform/gateway/gateway-controller/pkg/api/management" + "github.com/wso2/api-platform/gateway/gateway-controller/pkg/constants" ) // APIValidator validates API configurations using rule-based validation @@ -468,18 +469,22 @@ func validateResilienceTimeouts(fieldPrefix string, r *api.Resilience) []Validat if s == "" { return } - d, err := time.ParseDuration(s) - if err != nil { + // Enforce the same single-unit format as the CRD admission controller (see + // constants.ResilienceDurationPattern). This rejects compound durations ("1h30m"), + // negatives ("-30s"), and unitless values ("0", "30"), while accepting "0s" to disable. + if !constants.ResilienceDurationRegex.MatchString(s) { errors = append(errors, ValidationError{ Field: field, - Message: fmt.Sprintf("Invalid timeout format: %v (expected format: '30s', '1m', '500ms', or '0s' to disable)", err), + Message: "Invalid timeout format (expected a single-unit duration like '30s', '1m', '500ms', or '0s' to disable; compound, negative, and unitless values are not allowed)", }) return } - if d < 0 { + // The pattern guarantees a parseable, non-negative, single-unit value; ParseDuration is a + // final guard against pathological overflow. + if _, err := time.ParseDuration(s); err != nil { errors = append(errors, ValidationError{ Field: field, - Message: "Timeout must not be negative", + Message: fmt.Sprintf("Invalid timeout format: %v", err), }) } } diff --git a/gateway/gateway-controller/pkg/config/api_validator_test.go b/gateway/gateway-controller/pkg/config/api_validator_test.go index ec8c32cf55..8745ee6cdf 100644 --- a/gateway/gateway-controller/pkg/config/api_validator_test.go +++ b/gateway/gateway-controller/pkg/config/api_validator_test.go @@ -495,7 +495,13 @@ func TestAPIValidator_ValidateResilience(t *testing.T) { {name: "Invalid API-level timeout format", apiRes: &api.Resilience{Timeout: stringPtr("15seconds")}, wantError: true, errField: "spec.resilience.timeout"}, {name: "Invalid API-level idleTimeout format", apiRes: &api.Resilience{IdleTimeout: stringPtr("abc")}, wantError: true, errField: "spec.resilience.idleTimeout"}, {name: "Negative API-level timeout rejected", apiRes: &api.Resilience{Timeout: stringPtr("-5s")}, wantError: true, errField: "spec.resilience.timeout"}, + {name: "Compound API-level timeout rejected (must match CRD pattern)", apiRes: &api.Resilience{Timeout: stringPtr("1h30m")}, wantError: true, errField: "spec.resilience.timeout"}, + {name: "Compound API-level idleTimeout rejected", apiRes: &api.Resilience{IdleTimeout: stringPtr("1m30s")}, wantError: true, errField: "spec.resilience.idleTimeout"}, + {name: "Unitless API-level timeout rejected", apiRes: &api.Resilience{Timeout: stringPtr("30")}, wantError: true, errField: "spec.resilience.timeout"}, + {name: "Bare 0 rejected (unit required)", apiRes: &api.Resilience{Timeout: stringPtr("0")}, wantError: true, errField: "spec.resilience.timeout"}, + {name: "Fractional single-unit timeout allowed", apiRes: &api.Resilience{Timeout: stringPtr("1.5s")}, wantError: false}, {name: "Invalid operation-level timeout format", opRes: &api.Resilience{Timeout: stringPtr("nope")}, wantError: true, errField: "spec.operations[0].resilience.timeout"}, + {name: "Compound operation-level timeout rejected", opRes: &api.Resilience{Timeout: stringPtr("1h30m")}, wantError: true, errField: "spec.operations[0].resilience.timeout"}, } for _, tt := range tests { diff --git a/gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go b/gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go index 7523bfe48c..db67f81e7f 100644 --- a/gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go +++ b/gateway/gateway-controller/pkg/config/llm_validator_resilience_test.go @@ -76,6 +76,16 @@ func TestValidateLLMProvider_Resilience(t *testing.T) { assertHasFieldError(t, errs, "spec.resilience.timeout") }) + t.Run("compound timeout is rejected (must match CRD pattern)", func(t *testing.T) { + errs := validator.Validate(validProviderWithResilience(&api.Resilience{Timeout: stringPtr("1h30m")})) + assertHasFieldError(t, errs, "spec.resilience.timeout") + }) + + t.Run("0s is accepted (disables)", func(t *testing.T) { + errs := validator.Validate(validProviderWithResilience(&api.Resilience{Timeout: stringPtr("0s")})) + assert.Empty(t, errs) + }) + t.Run("negative timeout is rejected", func(t *testing.T) { errs := validator.Validate(validProviderWithResilience(&api.Resilience{Timeout: stringPtr("-5s")})) assertHasFieldError(t, errs, "spec.resilience.timeout") diff --git a/gateway/gateway-controller/pkg/constants/constants.go b/gateway/gateway-controller/pkg/constants/constants.go index cced5e512a..4833f0502d 100644 --- a/gateway/gateway-controller/pkg/constants/constants.go +++ b/gateway/gateway-controller/pkg/constants/constants.go @@ -179,6 +179,12 @@ const ( // System policy constants ANALYTICS_SYSTEM_POLICY_NAME = "wso2_apip_sys_analytics" ANALYTICS_SYSTEM_POLICY_VERSION = "v1" + + // ResilienceDurationPattern is the single source of truth for the format of resilience + // timeout strings. + // - accepted: "30s", "500ms", "1m", "2h", "1.5s", and "0s" (zero disables the timeout) + // - rejected: compound durations ("1h30m"), negatives ("-30s"), and unitless values ("0", "30") + ResilienceDurationPattern = `^\d+(\.\d+)?(ms|s|m|h)$` ) // DP->CP artifact push timing. The bottom-up (DP->CP) push waits for the local deployment @@ -199,3 +205,8 @@ var WILDCARD_HTTP_METHODS = []string{ "DELETE", "OPTIONS", } + +// ResilienceDurationRegex is the compiled ResilienceDurationPattern, shared by the management-API +// validator and the xDS downstream parser so both enforce exactly what the +// CRD admission controller enforces. +var ResilienceDurationRegex = regexp.MustCompile(ResilienceDurationPattern) \ No newline at end of file diff --git a/gateway/gateway-controller/pkg/xds/resilience_duration_test.go b/gateway/gateway-controller/pkg/xds/resilience_duration_test.go new file mode 100644 index 0000000000..714c99d83b --- /dev/null +++ b/gateway/gateway-controller/pkg/xds/resilience_duration_test.go @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package xds + +import "testing" + +// parseDurationAllowZero must accept exactly what the CRD admission controller accepts +// (constants.ResilienceDurationPattern): single-unit durations including "0s" to disable, while +// rejecting compound, negative, and unitless values. +func TestParseDurationAllowZero_MatchesCRDPattern(t *testing.T) { + ptr := func(s string) *string { return &s } + + t.Run("accepts single-unit and zero", func(t *testing.T) { + for _, in := range []string{"30s", "500ms", "1m", "2h", "1.5s", "0s", "0ms"} { + d, err := parseDurationAllowZero(ptr(in)) + if err != nil { + t.Errorf("expected %q to be accepted, got error: %v", in, err) + continue + } + if d == nil { + t.Errorf("expected %q to yield a non-nil duration", in) + } + } + }) + + t.Run("nil and empty yield nil without error", func(t *testing.T) { + for _, in := range []*string{nil, ptr(""), ptr(" ")} { + d, err := parseDurationAllowZero(in) + if err != nil || d != nil { + t.Errorf("expected nil,nil for empty input, got %v,%v", d, err) + } + } + }) + + t.Run("rejects compound, negative, and unitless", func(t *testing.T) { + for _, in := range []string{"1h30m", "1m30s", "-30s", "-5s", "30", "0", "15seconds", "abc"} { + if _, err := parseDurationAllowZero(ptr(in)); err == nil { + t.Errorf("expected %q to be rejected, but it was accepted", in) + } + } + }) +} diff --git a/gateway/gateway-controller/pkg/xds/translator.go b/gateway/gateway-controller/pkg/xds/translator.go index 6140529dcb..6943324b64 100644 --- a/gateway/gateway-controller/pkg/xds/translator.go +++ b/gateway/gateway-controller/pkg/xds/translator.go @@ -3076,20 +3076,23 @@ func parseTimeout(timeoutStr *string) (*time.Duration, error) { } // parseDurationAllowZero parses a duration string (e.g. "15s", "0s") into a *time.Duration. -// Unlike parseTimeout it accepts zero ("0s" means the timeout is explicitly disabled), -// rejecting only malformed and negative values. Returns nil for nil/empty input. +// Unlike parseTimeout it accepts zero ("0s" means the timeout is explicitly disabled). The format +// is enforced against constants.ResilienceDurationPattern so this downstream parser stays +// consistent with the CRD admission controller and the management-API validator: compound +// ("1h30m"), negative ("-30s"), and unitless ("0") values are rejected. Returns nil for nil/empty. func parseDurationAllowZero(timeoutStr *string) (*time.Duration, error) { if timeoutStr == nil || strings.TrimSpace(*timeoutStr) == "" { return nil, nil } - duration, err := time.ParseDuration(strings.TrimSpace(*timeoutStr)) - if err != nil { - return nil, fmt.Errorf("invalid timeout format: %w", err) + s := strings.TrimSpace(*timeoutStr) + if !constants.ResilienceDurationRegex.MatchString(s) { + return nil, fmt.Errorf("invalid timeout format %q: expected a single-unit duration like \"30s\", \"500ms\", or \"0s\" to disable", s) } - if duration < 0 { - return nil, fmt.Errorf("timeout must not be negative, got: %v", duration) + duration, err := time.ParseDuration(s) + if err != nil { + return nil, fmt.Errorf("invalid timeout format: %w", err) } return &duration, nil diff --git a/gateway/it/features/upstream-connect-timeout.feature b/gateway/it/features/upstream-connect-timeout.feature index b20d5b819f..ab855c50be 100644 --- a/gateway/it/features/upstream-connect-timeout.feature +++ b/gateway/it/features/upstream-connect-timeout.feature @@ -47,7 +47,7 @@ Feature: Timeouts timeout: connect: 6000ms upstreams: - - url: http://10.255.255.1:8080 + - url: http://192.0.2.1:8080 upstream: main: ref: my-timeout-upstream @@ -83,7 +83,7 @@ Feature: Timeouts upstreamDefinitions: - name: my-timeout-upstream-global upstreams: - - url: http://10.255.255.1:8080 + - url: http://192.0.2.1:8080 upstream: main: ref: my-timeout-upstream-global