Middle · Network services
Do returned nil but the response is a failure
An HTTP dependency returns 500 or an indefinitely large body. The Go caller checks only the error from Client.Do. What is missing?
Try explaining this aloud before opening the answer. State your assumptions and trace a boundary case.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"time"
)
func get(ctx context.Context, c *http.Client, url string, limit int64) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("upstream status %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
if err != nil {
return nil, err
}
if int64(len(body)) > limit {
return nil, fmt.Errorf("response exceeds %d bytes", limit)
}
return body, nil
}
func main() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("four")) }))
defer server.Close()
client := &http.Client{Timeout: 2 * time.Second}
_, err := get(context.Background(), client, server.URL, 3)
fmt.Println(err)
}Reveal a strong answer
A successful Client.Do means an HTTP response was received, not that the application operation succeeded. An error page can still be a valid response, and a server can keep streaming bytes after the headers arrive. Reuse the client and transport, pass an operation context and bound both elapsed time and bytes consumed. Closing the body is necessary even when status handling returns early.
Validate the configured positive byte limit and keep its arithmetic within range in a real API. Check status before parsing a success DTO, classify retryable failures and make unsafe writes retry only under an idempotency policy. Bound decompressed response bytes too. Reusing a transport enables connection pooling; reading to EOF can help reuse, but do not drain an untrusted infinite body merely to save a connection. The body is limited to the allowed bytes plus one detection byte, so a truncated success cannot be mistaken for a complete valid response. Client.Timeout covers the overall request including body reading; the context can impose an earlier bound. The sample uses only a local test server and returns the explicit oversize error.
Creating a new transport per request loses useful pooling and may waste sockets. Ignoring Close or using an unbounded ReadAll can exhaust memory. A timeout after sending a write leaves an unknown business outcome, even if no response body was received. Test slow headers, slow body, oversize decompressed data, non-2xx responses, redirects and cancellation in your native service.
Common trap
A successful transport exchange is not a successful or fully bounded business response.
Try it
Add operation time, status handling, a decompressed-byte limit, Close and a retry policy that distinguishes reads from ambiguous writes.
Worked solution
Do can return a non-2xx response with nil transport error. Check the status, bound body consumption and parse only the intended response shape. Always close the body, reuse the owned client/transport and do not drain an infinite hostile body just to preserve a connection. Client and context budgets include different limits that must be designed together. A timed-out write may have succeeded remotely, so retry only under an explicit idempotency/reconciliation contract.
Change the constraint
How do automatic redirects and compression change destination and byte-budget enforcement?
How did you do?
Self-assessment, not a compiler pass or interview score. Explain the reasoning unaided before marking understood.
Not marked. Progress stays on this browser, separate from course completion.
Primary references
Checked 2026-10-28. Use documentation for your pinned runtime, driver and tool version.