108 lines
3.5 KiB
Markdown
108 lines
3.5 KiB
Markdown
# Context in HTTP Servers & Service Calls
|
|
|
|
## Context in HTTP Servers
|
|
|
|
`http.Request` carries a context that is cancelled when the client disconnects or the request handler returns. MUST use `r.Context()` — NEVER create a new `context.Background()` inside a handler.
|
|
|
|
```go
|
|
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context() // this context is cancelled if the client disconnects
|
|
|
|
order, err := h.orderService.Get(ctx, r.PathValue("id"))
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
// Client disconnected, no point writing a response
|
|
return
|
|
}
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(order)
|
|
}
|
|
```
|
|
|
|
## Middleware enriching context
|
|
|
|
Middleware injects request-scoped values before handlers run. Use unexported key types to prevent collisions:
|
|
|
|
```go
|
|
// Helpers for trace propagation
|
|
type contextKey string
|
|
const (
|
|
traceIDKey contextKey = "trace_id"
|
|
spanIDKey contextKey = "span_id"
|
|
)
|
|
|
|
func TracingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
traceID := r.Header.Get("X-Trace-ID")
|
|
if traceID == "" {
|
|
traceID = generateTraceID()
|
|
}
|
|
spanID := r.Header.Get("X-Span-ID")
|
|
if spanID == "" {
|
|
spanID = generateSpanID()
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), traceIDKey, traceID)
|
|
ctx = context.WithValue(ctx, spanIDKey, spanID)
|
|
|
|
w.Header().Set("X-Trace-ID", traceID)
|
|
w.Header().Set("X-Span-ID", spanID)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// Propagate trace context to downstream services
|
|
func (c *HTTPClient) Do(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) {
|
|
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating request: %w", err)
|
|
}
|
|
|
|
if traceID, ok := ctx.Value(traceIDKey).(string); ok {
|
|
req.Header.Set("X-Trace-ID", traceID)
|
|
}
|
|
if spanID, ok := ctx.Value(spanIDKey).(string); ok {
|
|
req.Header.Set("X-Span-ID", spanID)
|
|
}
|
|
return c.client.Do(req)
|
|
}
|
|
```
|
|
|
|
## Context in Calls to Other Services
|
|
|
|
Context MUST be propagated to all HTTP clients and databases using context-aware APIs: `http.NewRequestWithContext`, `QueryContext`, `ExecContext`, and `QueryRowContext`. This ensures that client disconnections cancel all downstream operations.
|
|
|
|
```go
|
|
// ✗ Bad — downstream calls ignore the request context
|
|
func (c *PaymentClient) Charge(ctx context.Context, amount int) error {
|
|
req, _ := http.NewRequest("POST", c.url+"/charge", body)
|
|
return c.client.Do(req) // not context-aware
|
|
}
|
|
|
|
// ✓ Good — all downstream operations respect the context
|
|
func (c *PaymentClient) Charge(ctx context.Context, amount int) error {
|
|
req, err := http.NewRequestWithContext(ctx, "POST", c.url+"/charge", body)
|
|
if err != nil {
|
|
return fmt.Errorf("creating request: %w", err)
|
|
}
|
|
return c.client.Do(req)
|
|
}
|
|
```
|
|
|
|
```go
|
|
// ✗ Bad — downstream calls ignore the request context
|
|
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
|
|
row := r.db.QueryRow("SELECT * FROM users WHERE id = $1", id)
|
|
// ...
|
|
}
|
|
|
|
// ✓ Good — all downstream operations respect the context
|
|
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
|
|
row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
|
|
// ...
|
|
}
|
|
```
|