• English
  • Example: HTTP Client Cancel a Request

    An HTTP request may outlive the code that started it.

    A user may leave a page. A job may be canceled. An upstream operation may already have failed. Continuing to wait for the HTTP operation wastes time and resources.

    In Go, use the request's context.Context to control its lifetime.

    For an outgoing request, the context applies to the request exchange, including connection establishment, sending the request, waiting for response headers, and reading the response body.

    Quick Example

    package main
    
    import (
    	"context"
    	"fmt"
    	"net/http"
    	"time"
    )
    
    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel()
    
    	req, err := http.NewRequestWithContext(
    		ctx,
    		http.MethodGet,
    		"https://example.com/slow",
    		nil,
    	)
    	if err != nil {
    		panic(err)
    	}
    
    	go func() {
    		time.Sleep(100 * time.Millisecond)
    		cancel()
    	}()
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		fmt.Println("request canceled:", err)
    		return
    	}
    	defer resp.Body.Close()
    
    	fmt.Println(resp.Status)
    }

    The core pattern is:

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    req, err := http.NewRequestWithContext(
    	ctx,
    	http.MethodGet,
    	url,
    	nil,
    )

    Calling cancel() signals that the operation should stop.

    panic is used only to simplify the minimal demo. Production code must propagate and handle errors instead.

    Cancel When the Operation Is No Longer Needed

    Explicit cancellation is useful when an external event decides that the request should stop.

    ctx, cancel := context.WithCancel(parent)
    defer cancel()
    
    req, err := http.NewRequestWithContext(
    	ctx,
    	http.MethodGet,
    	url,
    	nil,
    )
    if err != nil {
    	return err
    }
    
    resp, err := client.Do(req)

    Later, another part of the program can call:

    cancel()

    This differs from a fixed time limit:

    Timeout:     "Has this taken too long?"
    Cancellation: "We no longer need this."

    Use WithCancel when cancellation comes from the lifecycle of the operation:

    • a user aborts an operation
    • a parent job is canceled
    • a worker shuts down
    • a result is no longer needed

    Cancel a Request After It Starts

    Cancellation does not have to happen before Do.

    The request may already be waiting for:

    • connection establishment
    • request transmission
    • response headers
    • response body data

    The request context remains associated with the HTTP operation after Do returns.

    A simple example with an external cancellation event:

    package main
    
    import (
    	"context"
    	"io"
    	"net/http"
    )
    
    func example(client *http.Client, url string) error {
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel()
    
    	req, err := http.NewRequestWithContext(
    		ctx,
    		http.MethodGet,
    		url,
    		nil,
    	)
    	if err != nil {
    		return err
    	}
    
    	result := make(chan error, 1)
    
    	go func() {
    		resp, err := client.Do(req)
    		if err != nil {
    			result <- err
    			return
    		}
    		defer resp.Body.Close()
    
    		_, err = io.Copy(io.Discard, resp.Body)
    		result <- err
    	}()
    
    	// Some external event decides the request is no longer needed.
    	cancel()
    
    	return <-result
    }

    The cancellation signal belongs to the request's context, not to a custom channel watched by the HTTP client.

    The channel above only communicates the result back to the caller.

    Do not build a custom HTTP cancellation mechanism when context.Context already provides the cancellation boundary.

    Cancellation Also Covers Response-Body Reads

    This is one of the most important details.

    Cancellation does not stop being relevant when Do returns.

    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()
    
    _, err = io.Copy(dst, resp.Body)

    If the context is canceled while the body read is blocked, the pending HTTP I/O can be interrupted.

    This matters especially for streaming responses:

    Do()
    
     ├── connect
     ├── send request
     ├── wait for headers
    
    
    read response body
    
     │ cancel()
    
    stop pending I/O

    You do not need a separate cancellation mechanism for resp.Body.

    There is an important boundary, however:

    Cancellation does not undo I/O that has already completed.

    If data has already been read from the network and is buffered in memory, canceling the context does not erase those bytes. Cancellation affects the operation's pending I/O; it is not a rollback mechanism.

    Cancel the Parent Operation

    In production code, an HTTP request usually should not create an unrelated lifetime.

    Instead, accept a context from the caller:

    func GetUser(
    	ctx context.Context,
    	client *http.Client,
    	url 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()
    
    	// Process response...
    
    	return nil
    }

    Now the caller controls the lifetime:

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    err := GetUser(ctx, client, url)

    In a larger application, the context usually already exists:

    func HandleRequest(ctx context.Context) error {
    	return GetUser(ctx, client, url)
    }

    The HTTP request becomes part of the larger operation instead of having an independent lifetime.

    This is especially important when the HTTP call is only one step in a larger workflow.

    Use WithTimeout for a Time Limit

    If the requirement is:

    Stop waiting after five seconds.

    Use a timeout context:

    ctx, cancel := context.WithTimeout(
    	context.Background(),
    	5*time.Second,
    )
    defer cancel()
    
    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()

    WithTimeout is cancellation with an automatic deadline.

    When the deadline expires, the context is canceled automatically.

    Always call the returned cancel function and defer it immediately:

    ctx, cancel := context.WithTimeout(parent, 5*time.Second)
    defer cancel()

    Even when the operation finishes normally, calling cancel releases resources associated with the derived context.

    The distinction is:

    context.WithCancel
        → lifecycle cancellation
    
    context.WithTimeout
        → deadline cancellation

    Context Timeout vs Client.Timeout

    Both can terminate HTTP requests, but they represent different ownership boundaries.

    A request-scoped timeout applies to one operation:

    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    req, err := http.NewRequestWithContext(
    	ctx,
    	method,
    	url,
    	body,
    )

    http.Client.Timeout applies to requests made through that client instance:

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

    Client.Timeout is a hard wall-clock limit covering the entire request lifecycle, including:

    • connection setup
    • redirects
    • sending the request
    • waiting for response headers
    • reading the response body

    It is not merely a connection timeout.

    A useful taxonomy is:

    context.WithCancel
        → this operation is no longer needed
    
    context.WithTimeout
        → this operation exceeded its allowed duration
    
    http.Client.Timeout
        → safety limit for requests using this client

    In practice, a request-specific context is usually the right place for an operation's deadline because it follows the operation through the call chain.

    A client-level timeout can still provide a useful safety guardrail.

    Don't Use Transport.CancelRequest

    Older Go code may contain:

    transport.CancelRequest(req)

    Do not use this for new code.

    Transport.CancelRequest was deprecated in Go 1.5. Request context cancellation is the modern mechanism and works across the HTTP transport's supported protocols.

    Prefer:

    ctx, cancel := context.WithCancel(parent)
    defer cancel()
    
    req, err := http.NewRequestWithContext(
    	ctx,
    	http.MethodGet,
    	url,
    	nil,
    )

    The cancellation policy is then attached directly to the request lifecycle.

    Cancellation Is an Error Condition

    After cancellation, Do normally returns an error rather than a successful HTTP response.

    resp, err := client.Do(req)
    if err != nil {
    	if errors.Is(err, context.Canceled) {
    		return fmt.Errorf("request canceled: %w", err)
    	}
    
    	if errors.Is(err, context.DeadlineExceeded) {
    		return fmt.Errorf("request deadline exceeded: %w", err)
    	}
    
    	return err
    }
    defer resp.Body.Close()

    Do not swallow cancellation:

    // Bad
    if errors.Is(err, context.Canceled) {
    	return nil
    }

    Cancellation is still an error condition for the operation.

    Whether the caller treats that error as expected or ignores it is an application-level decision. The HTTP helper should not silently convert cancellation into success.

    These cases are also useful to distinguish in logging and metrics:

    context.Canceled
        → caller actively aborted the operation
    
    context.DeadlineExceeded
        → operation exceeded its deadline
    
    other error
        → network, DNS, TLS, protocol, or other failure

    Do not treat context cancellation as equivalent to a server-side failure such as HTTP 500.

    Don't Forget to Close the Response Body

    Cancellation does not replace normal response-body ownership.

    When Do succeeds:

    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    You still need to close resp.Body.

    These solve different problems:

    context cancellation
        → stop pending I/O
    
    resp.Body.Close()
        → release the response body

    Use both where applicable.

    If you are reading a response body and cancellation occurs, the body should still be closed by the code that owns the response.

    Production Example

    A production HTTP helper should accept the caller's context instead of creating an isolated context.Background() internally.

    package api
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"io"
    	"net/http"
    )
    
    const maxResponseSize = 1 << 20 // 1 MiB
    
    func Get(
    	ctx context.Context,
    	client *http.Client,
    	url string,
    ) ([]byte, error) {
    	if client == nil {
    		return nil, errors.New("nil HTTP client")
    	}
    
    	req, err := http.NewRequestWithContext(
    		ctx,
    		http.MethodGet,
    		url,
    		nil,
    	)
    	if err != nil {
    		return nil, fmt.Errorf("create request: %w", err)
    	}
    
    	resp, err := client.Do(req)
    	if err != nil {
    		if errors.Is(err, context.Canceled) {
    			return nil, fmt.Errorf("request canceled: %w", err)
    		}
    
    		if errors.Is(err, context.DeadlineExceeded) {
    			return nil, fmt.Errorf("request deadline exceeded: %w", err)
    		}
    
    		return nil, fmt.Errorf("send request: %w", err)
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    		// Drain a small amount to help preserve connection reuse.
    		_, _ = io.CopyN(io.Discard, resp.Body, 4<<10)
    
    		return nil, fmt.Errorf(
    			"unexpected HTTP status: %s",
    			resp.Status,
    		)
    	}
    
    	body, err := io.ReadAll(
    		io.LimitReader(resp.Body, maxResponseSize+1),
    	)
    	if err != nil {
    		return nil, fmt.Errorf("read response: %w", err)
    	}
    
    	if len(body) > maxResponseSize {
    		return nil, fmt.Errorf(
    			"response exceeds %d bytes",
    			maxResponseSize,
    		)
    	}
    
    	return body, nil
    }

    The key design choice is:

    func Get(ctx context.Context, ...)

    The caller owns the operation's lifetime.

    Cancellation then propagates naturally:

    caller
    
      │ cancel
    
    context
    
    
    HTTP request
    
    
    response body

    The helper does not need its own cancellation channel, timeout goroutine, or global cancellation state.

    Common Mistakes

    1. Creating context.Background inside the HTTP helper

    // Bad
    func Get(client *http.Client, url string) error {
    	ctx := context.Background()
    	// ...
    }

    The caller can no longer cancel the request through its own context.

    Prefer:

    // Good
    func Get(
    	ctx context.Context,
    	client *http.Client,
    	url string,
    ) error {
    	// ...
    }

    2. Creating a timeout and discarding cancel

    // Bad
    ctx, _ := context.WithTimeout(parent, 5*time.Second)

    Prefer:

    // Good
    ctx, cancel := context.WithTimeout(parent, 5*time.Second)
    defer cancel()

    3. Assuming cancellation only affects Client.Do

    The request context also governs pending response-body reads.

    4. Assuming cancellation can undo completed I/O

    It cannot.

    Already-read or already-buffered data is not rolled back when the context is canceled.

    5. Using Transport.CancelRequest

    It is deprecated. Use request context cancellation for new code.

    6. Treating cancellation as a server failure

    An explicitly canceled operation is different from an HTTP 5xx response or a network failure.

    Keep those cases distinguishable in application metrics and logging.

    Rule of Thumb

    SituationUse
    Caller no longer needs the requestcontext.WithCancel
    Request has a fixed deadlinecontext.WithTimeout
    Existing operation already has a contextPass it through
    Need to stop pending response-body I/OCancel the request context
    Need a client-wide safety limithttp.Client.Timeout
    New HTTP request cancellationhttp.NewRequestWithContext

    The core pattern is:

    ctx, cancel := context.WithCancel(parent)
    defer cancel()
    
    req, err := http.NewRequestWithContext(
    	ctx,
    	http.MethodGet,
    	url,
    	nil,
    )
    if err != nil {
    	return err
    }
    
    resp, err := client.Do(req)

    The rule is simple:

    The caller owns the operation's lifetime. The request carries that lifetime through its context. Cancel the context when the operation is no longer needed.

    That is the standard cancellation boundary for Go HTTP clients.