Skip to content

Commit 56e5cd5

Browse files
Paranitharan1110Paranitharan Kalaiselvan
andauthored
feat: support configurable timeout via request.timeout in http builtin (kubevela#7239)
Signed-off-by: Paranitharan Kalaiselvan <paranitharan_kalaiselvan@gmail.com> Co-authored-by: Paranitharan Kalaiselvan <paranitharan_kalaiselvan@gmail.com>
1 parent 6fd0e71 commit 56e5cd5

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

pkg/builtin/http/http.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ func (c *HTTPCmd) Run(meta *registry.Meta) (res interface{}, err error) {
5959
}
6060
)
6161
if obj := meta.Obj.LookupPath(value.FieldPath("request")); obj.Exists() {
62+
// Allow CUE authors to override the default 3s timeout via request.timeout
63+
// (e.g., timeout: "30s"). Expects a string duration parseable by time.ParseDuration.
64+
// Only positive durations are accepted; zero or negative values are ignored.
65+
if v := obj.LookupPath(value.FieldPath("timeout")); v.Exists() {
66+
if timeoutStr, parseErr := v.String(); parseErr == nil {
67+
if d, parseErr := time.ParseDuration(timeoutStr); parseErr == nil && d > 0 {
68+
client.Timeout = d
69+
}
70+
}
71+
}
6272
if v := obj.LookupPath(value.FieldPath("body")); v.Exists() {
6373
r, err = v.Reader()
6474
if err != nil {

pkg/builtin/http/http_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"net/http"
2727
"net/http/httptest"
2828
"testing"
29+
"time"
2930

3031
"cuelang.org/go/cue"
3132
"cuelang.org/go/cue/cuecontext"
@@ -99,6 +100,110 @@ func TestHTTPCmdRun(t *testing.T) {
99100

100101
}
101102

103+
func TestHTTPCmdRunWithCustomTimeout(t *testing.T) {
104+
// Start a slow server that takes 200ms to respond
105+
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106+
time.Sleep(200 * time.Millisecond)
107+
w.WriteHeader(http.StatusOK)
108+
w.Write([]byte(`{"status":"ok"}`))
109+
}))
110+
l, err := net.Listen("tcp", "127.0.0.1:0")
111+
if err != nil {
112+
t.Fatalf("failed to listen on ephemeral port: %v", err)
113+
}
114+
ts.Listener.Close()
115+
ts.Listener = l
116+
ts.Start()
117+
defer ts.Close()
118+
119+
// Derive the server URL from the listener address
120+
serverURL := fmt.Sprintf("http://%s/api/v1/slow", l.Addr().String())
121+
122+
// Without custom timeout (default 3s) — should succeed since server responds in 200ms
123+
reqDefault := cuecontext.New().CompileString(fmt.Sprintf(`{
124+
method: "GET"
125+
url: "%s"
126+
}`, serverURL))
127+
runner, _ := newHTTPCmd(cue.Value{})
128+
got, err := runner.Run(&registry.Meta{Obj: reqDefault.Value()})
129+
assert.NoError(t, err)
130+
body := (got.(map[string]interface{}))["body"].(string)
131+
assert.Equal(t, `{"status":"ok"}`, body)
132+
133+
// With explicit timeout of 1s — should also succeed
134+
reqCustom := cuecontext.New().CompileString(fmt.Sprintf(`{
135+
method: "GET"
136+
url: "%s"
137+
request: {
138+
timeout: "1s"
139+
}
140+
}`, serverURL))
141+
got, err = runner.Run(&registry.Meta{Obj: reqCustom.Value()})
142+
assert.NoError(t, err)
143+
body = (got.(map[string]interface{}))["body"].(string)
144+
assert.Equal(t, `{"status":"ok"}`, body)
145+
146+
// With a very short timeout of 50ms — should fail
147+
reqShort := cuecontext.New().CompileString(fmt.Sprintf(`{
148+
method: "GET"
149+
url: "%s"
150+
request: {
151+
timeout: "50ms"
152+
}
153+
}`, serverURL))
154+
_, err = runner.Run(&registry.Meta{Obj: reqShort.Value()})
155+
assert.Error(t, err, "expected timeout error with 50ms deadline on a 200ms server")
156+
}
157+
158+
func TestHTTPCmdRunWithInvalidTimeout(t *testing.T) {
159+
s := NewMock()
160+
defer s.Close()
161+
162+
runner, _ := newHTTPCmd(cue.Value{})
163+
164+
// Invalid timeout value should be silently ignored and use the default 3s
165+
reqInvalid := cuecontext.New().CompileString(`{
166+
method: "GET"
167+
url: "http://127.0.0.1:8090/api/v1/token?val=test-token"
168+
request: {
169+
timeout: "not-a-duration"
170+
header: {
171+
"Accept-Language": "en,nl"
172+
}
173+
}
174+
}`)
175+
got, err := runner.Run(&registry.Meta{Obj: reqInvalid.Value()})
176+
assert.NoError(t, err)
177+
body := (got.(map[string]interface{}))["body"].(string)
178+
assert.Equal(t, `{"token":"test-token"}`, body)
179+
180+
// Zero timeout should be rejected and fall back to default 3s
181+
reqZero := cuecontext.New().CompileString(`{
182+
method: "GET"
183+
url: "http://127.0.0.1:8090/api/v1/token?val=test-zero"
184+
request: {
185+
timeout: "0s"
186+
}
187+
}`)
188+
got, err = runner.Run(&registry.Meta{Obj: reqZero.Value()})
189+
assert.NoError(t, err)
190+
body = (got.(map[string]interface{}))["body"].(string)
191+
assert.Equal(t, `{"token":"test-zero"}`, body)
192+
193+
// Negative timeout should be rejected and fall back to default 3s
194+
reqNegative := cuecontext.New().CompileString(`{
195+
method: "GET"
196+
url: "http://127.0.0.1:8090/api/v1/token?val=test-negative"
197+
request: {
198+
timeout: "-5s"
199+
}
200+
}`)
201+
got, err = runner.Run(&registry.Meta{Obj: reqNegative.Value()})
202+
assert.NoError(t, err)
203+
body = (got.(map[string]interface{}))["body"].(string)
204+
assert.Equal(t, `{"token":"test-negative"}`, body)
205+
}
206+
102207
func TestHTTPSRun(t *testing.T) {
103208
s := newMockHttpsServer()
104209
defer s.Close()

0 commit comments

Comments
 (0)