From 4493820e781dea4c0c3b4ce2f31cb6bf51483712 Mon Sep 17 00:00:00 2001 From: Adam Magued Date: Sat, 17 Oct 2026 17:55:23 +0300 Subject: [PATCH] fix: resolve issue #78 - Ignore responses from http.Head() Signed-off-by: Adam Magued --- passes/bodyclose/bodyclose.go | 48 ++++++++++- passes/bodyclose/testdata/src/a/issue78.go | 82 +++++++++++++++++++ .../testdata/src/consumption/consumption.go | 9 ++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 passes/bodyclose/testdata/src/a/issue78.go diff --git a/passes/bodyclose/bodyclose.go b/passes/bodyclose/bodyclose.go index 2a2eaa8..9808b19 100644 --- a/passes/bodyclose/bodyclose.go +++ b/passes/bodyclose/bodyclose.go @@ -181,8 +181,8 @@ func (r *runner) isopen(b *ssa.BasicBlock, i int) bool { return false } - if len(*call.Referrers()) == 0 { - return true + if r.isHeadCall(call) { + return false } if instr, ok := b.Instrs[i].(*ssa.Call); ok { @@ -196,6 +196,10 @@ func (r *runner) isopen(b *ssa.BasicBlock, i int) bool { } } + if len(*call.Referrers()) == 0 { + return true + } + cRefs := *call.Referrers() for _, cRef := range cRefs { val, ok := r.getResVal(cRef) @@ -338,6 +342,46 @@ func (r *runner) getReqCall(instr ssa.Instruction) (*ssa.Call, bool) { return call, true } +func (r *runner) isHeadCall(call *ssa.Call) bool { + if callee := call.Call.StaticCallee(); callee != nil { + if callee.Name() != "Head" { + return false + } + if callee.Pkg == nil || callee.Pkg.Pkg == nil { + return false + } + if analysisutil.RemoveVendor(callee.Pkg.Pkg.Path()) != nethttpPath { + return false + } + recv := callee.Signature.Recv() + if recv == nil { + return true + } + typ := recv.Type() + if ptr, ok := typ.(*types.Pointer); ok { + typ = ptr.Elem() + } + return isNamedType(typ, nethttpPath, "Client") + } + + if call.Call.IsInvoke() { + method := call.Call.Method + if method != nil && method.Name() == "Head" { + sig, ok := method.Type().(*types.Signature) + if ok && sig.Params().Len() >= 1 && sig.Results().Len() == 2 { + res0 := sig.Results().At(0).Type().String() + res1 := sig.Results().At(1).Type().String() + if res0 == r.resTyp.String() && res1 == "error" { + return true + } + } + } + } + + return false +} + + func (r *runner) getResVal(instr ssa.Instruction) (ssa.Value, bool) { switch instr := instr.(type) { case *ssa.FieldAddr: diff --git a/passes/bodyclose/testdata/src/a/issue78.go b/passes/bodyclose/testdata/src/a/issue78.go new file mode 100644 index 0000000..4396e88 --- /dev/null +++ b/passes/bodyclose/testdata/src/a/issue78.go @@ -0,0 +1,82 @@ +package a + +import ( + "fmt" + "net/http" +) + +type HeadClient interface { + Head(url string) (*http.Response, error) +} + +func issue78() { + // Call http.Head directly without assigning return values + http.Head("http://example.com") // OK + + // Call http.Head ignoring return values + _, _ = http.Head("http://example.com") // OK + + // Call http.Head and inspect response without closing body + resp, err := http.Head("http://example.com") // OK + if err != nil { + return + } + fmt.Println(resp.StatusCode) + + // Call http.Head and close body explicitly + respWithClose, err := http.Head("http://example.com") // OK + if err != nil { + return + } + defer respWithClose.Body.Close() + + // Call client.Head on *http.Client without closing body + client := &http.Client{} + respClient, err := client.Head("http://example.com") // OK + if err != nil { + return + } + fmt.Println(respClient.Status) + + // Call client.Head directly without assigning + client.Head("http://example.com") // OK + + // Call client.Head on http.Client value without closing body + var valClient http.Client + respValClient, err := valClient.Head("http://example.com") // OK + if err != nil { + return + } + fmt.Println(respValClient.Header) + + // Call http.DefaultClient.Head without closing body + respDefault, err := http.DefaultClient.Head("http://example.com") // OK + if err != nil { + return + } + fmt.Println(respDefault.StatusCode) +} + +func issue78Interface(c HeadClient) { + resp, err := c.Head("http://example.com") // OK + if err != nil { + return + } + fmt.Println(resp.StatusCode) +} + +func issue78RegressionGet() { + resp, err := http.Get("http://example.com") // want "response body must be closed" + if err != nil { + return + } + fmt.Println(resp.StatusCode) +} + +func issue78RegressionClientGet(client *http.Client) { + resp, err := client.Get("http://example.com") // want "response body must be closed" + if err != nil { + return + } + fmt.Println(resp.StatusCode) +} diff --git a/passes/bodyclose/testdata/src/consumption/consumption.go b/passes/bodyclose/testdata/src/consumption/consumption.go index 30d2eb9..ca07ec2 100644 --- a/passes/bodyclose/testdata/src/consumption/consumption.go +++ b/passes/bodyclose/testdata/src/consumption/consumption.go @@ -209,6 +209,15 @@ func responseHandledDirectiveSkipsConsumption() { _, _ = handledResponse() } +func headRequestSkipsConsumption() { + resp, err := http.Head("http://example.com") // OK + if err != nil { + return + } + _ = resp.StatusCode +} + + // closeBeforeConsume is commented out because current implementation // doesn't detect execution order (documented limitation) //