feat: support configurable timeout via request.timeout in http builtin (#7239)

Signed-off-by: Paranitharan Kalaiselvan <paranitharan_kalaiselvan@gmail.com>
Co-authored-by: Paranitharan Kalaiselvan <paranitharan_kalaiselvan@gmail.com>
This commit is contained in:
Paranitharan Kalaiselvan
2026-07-17 15:11:34 +05:30
committed by GitHub
parent 6fd0e71e50
commit 56e5cd5ff5
2 changed files with 115 additions and 0 deletions

View File

@@ -59,6 +59,16 @@ func (c *HTTPCmd) Run(meta *registry.Meta) (res interface{}, err error) {
}
)
if obj := meta.Obj.LookupPath(value.FieldPath("request")); obj.Exists() {
// Allow CUE authors to override the default 3s timeout via request.timeout
// (e.g., timeout: "30s"). Expects a string duration parseable by time.ParseDuration.
// Only positive durations are accepted; zero or negative values are ignored.
if v := obj.LookupPath(value.FieldPath("timeout")); v.Exists() {
if timeoutStr, parseErr := v.String(); parseErr == nil {
if d, parseErr := time.ParseDuration(timeoutStr); parseErr == nil && d > 0 {
client.Timeout = d
}
}
}
if v := obj.LookupPath(value.FieldPath("body")); v.Exists() {
r, err = v.Reader()
if err != nil {

View File

@@ -26,6 +26,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
@@ -99,6 +100,110 @@ func TestHTTPCmdRun(t *testing.T) {
}
func TestHTTPCmdRunWithCustomTimeout(t *testing.T) {
// Start a slow server that takes 200ms to respond
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(200 * time.Millisecond)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}))
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen on ephemeral port: %v", err)
}
ts.Listener.Close()
ts.Listener = l
ts.Start()
defer ts.Close()
// Derive the server URL from the listener address
serverURL := fmt.Sprintf("http://%s/api/v1/slow", l.Addr().String())
// Without custom timeout (default 3s) — should succeed since server responds in 200ms
reqDefault := cuecontext.New().CompileString(fmt.Sprintf(`{
method: "GET"
url: "%s"
}`, serverURL))
runner, _ := newHTTPCmd(cue.Value{})
got, err := runner.Run(&registry.Meta{Obj: reqDefault.Value()})
assert.NoError(t, err)
body := (got.(map[string]interface{}))["body"].(string)
assert.Equal(t, `{"status":"ok"}`, body)
// With explicit timeout of 1s — should also succeed
reqCustom := cuecontext.New().CompileString(fmt.Sprintf(`{
method: "GET"
url: "%s"
request: {
timeout: "1s"
}
}`, serverURL))
got, err = runner.Run(&registry.Meta{Obj: reqCustom.Value()})
assert.NoError(t, err)
body = (got.(map[string]interface{}))["body"].(string)
assert.Equal(t, `{"status":"ok"}`, body)
// With a very short timeout of 50ms — should fail
reqShort := cuecontext.New().CompileString(fmt.Sprintf(`{
method: "GET"
url: "%s"
request: {
timeout: "50ms"
}
}`, serverURL))
_, err = runner.Run(&registry.Meta{Obj: reqShort.Value()})
assert.Error(t, err, "expected timeout error with 50ms deadline on a 200ms server")
}
func TestHTTPCmdRunWithInvalidTimeout(t *testing.T) {
s := NewMock()
defer s.Close()
runner, _ := newHTTPCmd(cue.Value{})
// Invalid timeout value should be silently ignored and use the default 3s
reqInvalid := cuecontext.New().CompileString(`{
method: "GET"
url: "http://127.0.0.1:8090/api/v1/token?val=test-token"
request: {
timeout: "not-a-duration"
header: {
"Accept-Language": "en,nl"
}
}
}`)
got, err := runner.Run(&registry.Meta{Obj: reqInvalid.Value()})
assert.NoError(t, err)
body := (got.(map[string]interface{}))["body"].(string)
assert.Equal(t, `{"token":"test-token"}`, body)
// Zero timeout should be rejected and fall back to default 3s
reqZero := cuecontext.New().CompileString(`{
method: "GET"
url: "http://127.0.0.1:8090/api/v1/token?val=test-zero"
request: {
timeout: "0s"
}
}`)
got, err = runner.Run(&registry.Meta{Obj: reqZero.Value()})
assert.NoError(t, err)
body = (got.(map[string]interface{}))["body"].(string)
assert.Equal(t, `{"token":"test-zero"}`, body)
// Negative timeout should be rejected and fall back to default 3s
reqNegative := cuecontext.New().CompileString(`{
method: "GET"
url: "http://127.0.0.1:8090/api/v1/token?val=test-negative"
request: {
timeout: "-5s"
}
}`)
got, err = runner.Run(&registry.Meta{Obj: reqNegative.Value()})
assert.NoError(t, err)
body = (got.(map[string]interface{}))["body"].(string)
assert.Equal(t, `{"token":"test-negative"}`, body)
}
func TestHTTPSRun(t *testing.T) {
s := newMockHttpsServer()
defer s.Close()