From c664c338c2806a70bfbea5bdbb7c5a1409120396 Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:07:37 -0400 Subject: [PATCH] fix: detect zero HTTP client timeouts --- .../reliability/reliability_go_helpers.go | 30 ++++++++++++++++++- tests/checks/reliability_test.go | 28 +++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/internal/codeguard/checks/reliability/reliability_go_helpers.go b/internal/codeguard/checks/reliability/reliability_go_helpers.go index 0b33962..b4bf870 100644 --- a/internal/codeguard/checks/reliability/reliability_go_helpers.go +++ b/internal/codeguard/checks/reliability/reliability_go_helpers.go @@ -3,6 +3,8 @@ package reliability import ( "fmt" "go/ast" + "go/token" + "math/big" "strings" ) @@ -195,7 +197,7 @@ func isHTTPClientWithTimeout(expr ast.Expr, aliases map[string]struct{}) bool { continue } if key, ok := kv.Key.(*ast.Ident); ok && key.Name == "Timeout" { - return true + return !isZeroDuration(kv.Value) } } case *ast.CallExpr: @@ -204,6 +206,32 @@ func isHTTPClientWithTimeout(expr ast.Expr, aliases map[string]struct{}) bool { return false } +func isZeroDuration(expr ast.Expr) bool { + switch n := expr.(type) { + case *ast.ParenExpr: + return isZeroDuration(n.X) + case *ast.UnaryExpr: + return (n.Op == token.ADD || n.Op == token.SUB) && isZeroDuration(n.X) + case *ast.BasicLit: + if n.Kind != token.INT { + return false + } + value, ok := new(big.Int).SetString(strings.ReplaceAll(n.Value, "_", ""), 0) + return ok && value.Sign() == 0 + case *ast.CallExpr: + // Duration conversions, such as time.Duration(0), preserve zero. + return len(n.Args) == 1 && isZeroDuration(n.Args[0]) + case *ast.BinaryExpr: + if n.Op == token.MUL { + return isZeroDuration(n.X) || isZeroDuration(n.Y) + } + if n.Op == token.ADD || n.Op == token.SUB { + return isZeroDuration(n.X) && isZeroDuration(n.Y) + } + } + return false +} + func isHTTPClientType(expr ast.Expr, aliases map[string]struct{}) bool { selector, ok := expr.(*ast.SelectorExpr) if !ok || selector.Sel.Name != "Client" { diff --git a/tests/checks/reliability_test.go b/tests/checks/reliability_test.go index bbfb4d3..1adcaa2 100644 --- a/tests/checks/reliability_test.go +++ b/tests/checks/reliability_test.go @@ -162,3 +162,31 @@ func Fetch(ctx context.Context, url string) error { assertFindingRuleAbsent(t, report, "Reliability", "reliability.missing-timeout") assertFindingRuleAbsent(t, report, "Reliability", "reliability.resource-leak") } + +func TestReliabilityGoFlagsZeroClientTimeout(t *testing.T) { + for _, timeout := range []string{"0", "time.Duration(0)", "0 * time.Second"} { + t.Run(timeout, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "client.go"), `package sample + +import ( + "net/http" + "time" +) + +func Fetch(req *http.Request) error { + client := &http.Client{Timeout: `+timeout+`} + _, err := client.Do(req) + return err +} +`) + + report, err := codeguard.Run(context.Background(), reliabilityConfig("reliability-zero-timeout", dir)) + if err != nil { + t.Fatalf("run: %v", err) + } + + assertFindingRulePresent(t, report, "Reliability", "reliability.missing-timeout") + }) + } +}