Skip to content

Day 039 — net/http Client & Requests

Month 2 · Week 2 · ⬅ Day 038 · Day 040 ➡ · Journal index

🎯 Learning Objective

Make HTTP requests correctly with net/http: build requests, configure a client with a timeout, read and always close response bodies, and treat status codes as data rather than errors.

📚 Topics

  • http.Client{Timeout} vs http.DefaultClient / http.Get
  • http.NewRequest / NewRequestWithContext · client.Do
  • resp.Body is an io.ReadClosermust Close
  • Status codes are not Go errors · headers · request bodies
  • httptest.Server for testing offline

📖 Reading / Sources

📝 Notes

  • Never ship http.Get/http.DefaultClient in production: they have no timeout, so a stuck server hangs your goroutine forever. Construct &http.Client{Timeout: 5 * time.Second} (whole-request budget) → [[http-default-no-timeout]].
  • Build requests with http.NewRequestWithContext(ctx, method, url, body); the body is an io.Reader (e.g. strings.NewReader, bytes.NewReader) or nil. Then call client.Do(req).
  • resp.Body is an io.ReadCloser you MUST closedefer resp.Body.Close() — even on non-2xx responses. Not closing leaks connections and prevents keep-alive reuse → [[always-close-body]].
  • A 404 or 500 is not a Go error. client.Do returns an error only for transport-level failures (DNS, refused, timeout, cancellation). Inspect resp.StatusCode / resp.Status yourself → [[status-is-data]].
  • To consume the body once, io.ReadAll(resp.Body) — wrap it in io.LimitReader to cap untrusted responses. For JSON, json.NewDecoder(resp.Body).Decode(&v) streams it.
  • Set request headers via req.Header.Set("Content-Type", "application/json"); read response headers from resp.Header.Get(...).
  • One http.Client is safe for concurrent use and pools connections via its Transport. Create one and share it; don't make a client per request.
  • Test HTTP without the network using httptest.NewServer(handler) — it listens on a real loopback port in-process, so example code stays stdlib-only and offline.

💻 Code Examples

client := &http.Client{Timeout: 5 * time.Second}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
    return err
}
resp, err := client.Do(req)
if err != nil {
    return err // transport error only
}
defer resp.Body.Close() // mandatory

if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("unexpected status %s", resp.Status)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))

Full code: examples/month-02/http-context/main.go · Run: go run ./examples/month-02/http-context

🏋️ Exercises / Practice

Exercise Status Link
Context-aware fetch in the combined http+context example examples/month-02/http-context

🐛 Mistakes Made

  • Forgot defer resp.Body.Close() → connections piled up under load (no keep-alive reuse).
  • Treated a returned resp with status 500 as success because err == nil — had to check StatusCode.
  • Used http.Get in a service and watched a goroutine hang forever on a slow upstream (no timeout).

❓ Open Questions

  • When should I tune http.Transport (MaxIdleConnsPerHost, DialContext timeouts) vs. rely on Client.Timeout?

🧠 Active Recall (answer without looking)

  1. Q: client.Do returned err == nil and resp.StatusCode == 404. Did the request "fail"?

    A Not at the Go level — the transport succeeded, so `err` is nil. A 404 is application data; you must check `resp.StatusCode` yourself. `Do` only errors on transport problems (DNS, refused, timeout, context cancel).

  2. Q: Name two things that go wrong if you don't close resp.Body.

    A You leak the underlying TCP connection (file descriptor), and you prevent the connection from being reused for keep-alive — both degrade throughput and can exhaust resources. Always `defer resp.Body.Close()` once `err == nil`.

🪶 Feynman Reflection

An http.Client is a configured phone you reuse for every call; each call is a Request you dial with Do. The reply (Response) hands you an open pipe (Body) you must read and then hang up (Close), or the line stays busy. Whether the other side said "OK" or "not found" is just what they told you — it's never a dialing error.

🕳️ Knowledge Gaps

  • Custom Transport tuning and connection-pool sizing.
  • Retrying idempotent requests safely (interacts with tomorrow's context work).

✅ Summary

I can build context-bound requests, use a timeout-configured shared client, always close bodies, and read status codes as data — testing it all offline with httptest.

⏭️ Next Steps / Prep for Tomorrow

  • Day 040: context — propagating deadlines and cancellation through that request (and everything else).

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): net/http client and requests (day 039)