From 1aad1a32929898072b4f95ee800970493f4ac48a Mon Sep 17 00:00:00 2001 From: Jsp Date: Sat, 29 Aug 2026 18:51:13 +0300 Subject: [PATCH] fix(net): report the status code when the response body cannot be read GetCurl documents that the "status code return as is even in unmarshalling error", and the json.Unmarshal path five lines below does exactly that. The io.ReadAll path returned 0 instead, so a caller that branches on the status could not tell a 503 whose body was truncated from a request that never reached the server at all. This matters for the package's own users: getIPViaTraceURL wraps the result as "request failed, code: %d", which logged code 0 for a server that had in fact answered. Return resp.StatusCode on that path too, matching the sibling error return and the documented contract. --- pkg/net/request.go | 2 +- pkg/net/request_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pkg/net/request.go b/pkg/net/request.go index 48c72d6..da6d2ea 100644 --- a/pkg/net/request.go +++ b/pkg/net/request.go @@ -110,7 +110,7 @@ func executeRequest[T any](req *stdhttp.Request, config *CurlConf[T]) (res *T, s } b, err := io.ReadAll(resp.Body) if err != nil { - return res, 0, fmt.Errorf("failed to read response body: %w", err) + return res, resp.StatusCode, fmt.Errorf("failed to read response body: %w", err) } var result T if strings.TrimSpace(strings.ReplaceAll(string(b), "\"", "")) != "" { diff --git a/pkg/net/request_test.go b/pkg/net/request_test.go index b7ae3cc..86970ed 100644 --- a/pkg/net/request_test.go +++ b/pkg/net/request_test.go @@ -3,6 +3,7 @@ package net_test import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -337,3 +338,36 @@ func newTestServer(t *testing.T, handler func(w http.ResponseWriter, r *http.Req t.Cleanup(srv.Close) return srv } + +// errReadCloser fails on the first Read so the body cannot be consumed. +type errReadCloser struct{} + +func (errReadCloser) Read([]byte) (int, error) { return 0, errors.New("connection reset") } +func (errReadCloser) Close() error { return nil } + +// bodyFailRoundTripper answers with a real status code but an unreadable body. +type bodyFailRoundTripper struct{ status int } + +func (rt bodyFailRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: rt.status, + Header: make(http.Header), + Body: errReadCloser{}, + }, nil +} + +func TestCurlReportsStatusWhenBodyReadFails(t *testing.T) { + // given a server that answers 503 but whose body cannot be read + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + client := &http.Client{Transport: bodyFailRoundTripper{status: http.StatusServiceUnavailable}} + + // when + res, code, err := netpkg.GetCurl[TestResponse](ctx, "http://example.invalid/test", nil, + netpkg.WithHTTPClient[TestResponse](client)) + + // then the caller still learns the status, as it does for a decode failure + require.ErrorContains(t, err, "failed to read response body") + require.Equal(t, http.StatusServiceUnavailable, code, "status code should survive a body read failure") + require.Nil(t, res) +}