Skip to content

Commit b36b2fa

Browse files
authored
feat: Show HTTP response body on 500 (#84)
#### PR Dependency Tree * **PR #84** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
2 parents 02657eb + 364288b commit b36b2fa

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

transport/http/channel.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package http
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"net/http"
78
"net/url"
89

@@ -77,7 +78,22 @@ func (c *Channel) Request(ctx context.Context, req transport.HTTPRequest) (trans
7778
return nil, fmt.Errorf("doing HTTP request: %w", err)
7879
}
7980
if !slices.Contains(c.statuses, res.StatusCode) {
80-
return nil, NewHTTPError(fmt.Sprintf("HTTP Request failed. %s %s → %d", hr.Method, c.url.String(), res.StatusCode), res.StatusCode, res.Header)
81+
const maxBodyLen = 4096
82+
b, readErr := io.ReadAll(io.LimitReader(res.Body, maxBodyLen+1))
83+
res.Body.Close()
84+
85+
msg := fmt.Sprintf("HTTP Request failed. %s %s → %d", hr.Method, c.url.String(), res.StatusCode)
86+
if readErr != nil {
87+
msg += fmt.Sprintf(" (failed to read response body: %s)", readErr)
88+
} else if len(b) > 0 {
89+
body := string(b)
90+
if len(body) > maxBodyLen {
91+
body = body[:maxBodyLen] + "... (truncated)"
92+
}
93+
msg += ": " + body
94+
}
95+
96+
return nil, NewHTTPError(msg, res.StatusCode, res.Header)
8197
}
8298

8399
ctx = extractTraceContext(ctx, res.Header)

transport/http/channel_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package http
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
67
"net/http/httptest"
78
"net/url"
9+
"strings"
810
"testing"
911

1012
"go.opentelemetry.io/otel"
@@ -97,6 +99,83 @@ func mustSpanIDFromHex(t *testing.T, hex string) trace.SpanID {
9799
return spanID
98100
}
99101

102+
func TestChannelErrorIncludesResponseBody(t *testing.T) {
103+
const errorBody = `{"error":"InternalServerError","message":"something went wrong"}`
104+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
105+
w.WriteHeader(http.StatusInternalServerError)
106+
w.Write([]byte(errorBody))
107+
}))
108+
t.Cleanup(server.Close)
109+
110+
endpoint, err := url.Parse(server.URL)
111+
if err != nil {
112+
t.Fatalf("parsing server URL: %v", err)
113+
}
114+
115+
channel := NewChannel(endpoint, WithClient(server.Client()))
116+
117+
_, err = channel.Request(context.Background(), NewRequest(http.NoBody, nil))
118+
if err == nil {
119+
t.Fatal("expected error, got nil")
120+
}
121+
122+
if !strings.Contains(err.Error(), errorBody) {
123+
t.Fatalf("expected error to contain response body %q, got: %s", errorBody, err.Error())
124+
}
125+
}
126+
127+
func TestChannelErrorHandlesEmptyBody(t *testing.T) {
128+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129+
w.WriteHeader(http.StatusInternalServerError)
130+
}))
131+
t.Cleanup(server.Close)
132+
133+
endpoint, err := url.Parse(server.URL)
134+
if err != nil {
135+
t.Fatalf("parsing server URL: %v", err)
136+
}
137+
138+
channel := NewChannel(endpoint, WithClient(server.Client()))
139+
140+
_, err = channel.Request(context.Background(), NewRequest(http.NoBody, nil))
141+
if err == nil {
142+
t.Fatal("expected error, got nil")
143+
}
144+
145+
expected := fmt.Sprintf("HTTP Request failed. POST %s → 500", server.URL)
146+
if err.Error() != expected {
147+
t.Fatalf("expected %q, got %q", expected, err.Error())
148+
}
149+
}
150+
151+
func TestChannelErrorTruncatesLongBody(t *testing.T) {
152+
longBody := strings.Repeat("x", 5000)
153+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
154+
w.WriteHeader(http.StatusInternalServerError)
155+
w.Write([]byte(longBody))
156+
}))
157+
t.Cleanup(server.Close)
158+
159+
endpoint, err := url.Parse(server.URL)
160+
if err != nil {
161+
t.Fatalf("parsing server URL: %v", err)
162+
}
163+
164+
channel := NewChannel(endpoint, WithClient(server.Client()))
165+
166+
_, err = channel.Request(context.Background(), NewRequest(http.NoBody, nil))
167+
if err == nil {
168+
t.Fatal("expected error, got nil")
169+
}
170+
171+
if !strings.Contains(err.Error(), "... (truncated)") {
172+
t.Fatalf("expected error to contain truncation marker, got: %s", err.Error())
173+
}
174+
if strings.Contains(err.Error(), longBody) {
175+
t.Fatal("expected error to NOT contain the full body")
176+
}
177+
}
178+
100179
func setTraceContextPropagator() func() {
101180
prev := otel.GetTextMapPropagator()
102181
otel.SetTextMapPropagator(propagation.TraceContext{})

0 commit comments

Comments
 (0)