package main

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

// fetch passes the caller's cancellation into both the HTTP request and body read.
func fetch(ctx context.Context, client *http.Client, url string) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return "", err
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("upstream returned %s", resp.Status)
	}
	const limit = 1 << 20
	body, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
	if err != nil {
		return "", err
	}
	if len(body) > limit {
		return "", errors.New("upstream response exceeds 1 MiB")
	}
	return string(body), nil
}

// The handler already runs concurrently with other requests. A single blocking,
// context-aware fetch does not require another goroutine and a result channel.
func handler(client *http.Client, url string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
		defer cancel()
		body, err := fetch(ctx, client, url)
		if err != nil {
			if r.Context().Err() != nil {
				return // The caller no longer needs a response.
			}
			status := http.StatusBadGateway
			if errors.Is(err, context.DeadlineExceeded) {
				status = http.StatusGatewayTimeout
			}
			http.Error(w, http.StatusText(status), status)
			return
		}
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		_, _ = io.WriteString(w, body)
	}
}

func main() {
	// A local upstream replaces a real dependency so this demo needs no internet.
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		timer := time.NewTimer(200 * time.Millisecond)
		defer timer.Stop()
		select {
		case <-timer.C:
			_, _ = io.WriteString(w, "upstream finished\n")
		case <-r.Context().Done():
			return
		}
	}))
	defer upstream.Close()
	api := httptest.NewServer(handler(upstream.Client(), upstream.URL))
	defer api.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()
	_, err := fetch(ctx, api.Client(), api.URL)
	fmt.Println("request deadline exceeded:", errors.Is(err, context.DeadlineExceeded))
}
