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}vshttp.DefaultClient/http.Gethttp.NewRequest/NewRequestWithContext·client.Doresp.Bodyis anio.ReadCloser— must Close- Status codes are not Go errors · headers · request bodies
httptest.Serverfor testing offline
📖 Reading / Sources¶
📝 Notes¶
- Never ship
http.Get/http.DefaultClientin 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 anio.Reader(e.g.strings.NewReader,bytes.NewReader) ornil. Then callclient.Do(req). resp.Bodyis anio.ReadCloseryou MUST close —defer 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.Doreturns an error only for transport-level failures (DNS, refused, timeout, cancellation). Inspectresp.StatusCode/resp.Statusyourself → [[status-is-data]]. - To consume the body once,
io.ReadAll(resp.Body)— wrap it inio.LimitReaderto 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 fromresp.Header.Get(...). - One
http.Clientis safe for concurrent use and pools connections via itsTransport. 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
respwith status 500 as success becauseerr == nil— had to checkStatusCode. - Used
http.Getin 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 onClient.Timeout?
🧠 Active Recall (answer without looking)¶
-
Q:
client.Doreturnederr == nilandresp.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). -
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
Transporttuning 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)