Go HTTP Request Timeout with Context

Apply a per-request deadline with context.WithTimeout, classify timeout errors, and cap response reads in a Go HTTP client.

GoData Fetching

Code

snippet
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"time"
)

type fetchResult struct {
	Status int
	Body   string
}

func fetchWithDeadline(ctx context.Context, client *http.Client, url string, timeout time.Duration) (fetchResult, error) {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return fetchResult{}, fmt.Errorf("build request: %w", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
			return fetchResult{}, fmt.Errorf("fetch %s: deadline exceeded after %s: %w", url, timeout, context.DeadlineExceeded)
		}
		return fetchResult{}, fmt.Errorf("fetch %s: %w", url, err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return fetchResult{}, fmt.Errorf("read body: %w", err)
	}
	return fetchResult{Status: resp.StatusCode, Body: string(body)}, nil
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "ok")
	})
	mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
		select {
		case <-time.After(200 * time.Millisecond):
			fmt.Fprint(w, "late")
		case <-r.Context().Done():
		}
	})
	server := httptest.NewServer(mux)
	defer server.Close()

	client := &http.Client{Timeout: 5 * time.Second}
	ctx := context.Background()

	result, err := fetchWithDeadline(ctx, client, server.URL+"/fast", 500*time.Millisecond)
	fmt.Printf("fast: status=%d body=%q err=%v\n", result.Status, result.Body, err)

	result, err = fetchWithDeadline(ctx, client, server.URL+"/slow", 50*time.Millisecond)
	fmt.Printf("slow: status=%d body=%q err=%v\n", result.Status, result.Body, err)
	if errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("slow request classified as a deadline error")
	}
}