hashicorp / go-retryablehttp

Retryable HTTP client in Go
Mozilla Public License 2.0
1.99k stars 251 forks source link

Return last not nil response on context Canceled/DeadlineExceeded errors #129

Open adw1n opened 3 years ago

adw1n commented 3 years ago

I think it would be good to give the user access to last response on .Do(req) errors. That is assuming there was at least one request for which we got a response. As a user when a timeout or context cancellation happens I'd like to have access to last response because based on the response status code and body I can make certain decisions on how the error should be handled.

@ryanuber @jefferai are maintainers open to accepting this change?

Possible solutions (subjectively ranked by my preference):

  1. retrun lastNotNilResponse, err from .Do(req) e.g. from this place https://github.com/hashicorp/go-retryablehttp/blob/991b9d0a42d13014e3689dd49a94c02be01f4237/client.go#L654
  2. create custom error type that holds the error + last response and supports errors.Is etc. checks
  3. provide a new ErrorHandler factory that returns an error handler like PassthroughErrorHandler. The created error handler would store not nil responses to a user defined variable that was passed to the factory when creating the ErrorHandler.
  4. save lastResponse somewhere on the retryablehttp.Client object

Minimal working example with a comment explaining the change that I'd like to see:

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "github.com/hashicorp/go-retryablehttp"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
    go func() {
        time.Sleep(time.Second * 5)
        cancel()
    }()
    req, err := http.NewRequestWithContext(ctx, "GET", "https://httpstat.us/500", nil)
    if err != nil {
        panic(err)
    }
    retryReq, err := retryablehttp.FromRequest(req)
    if err != nil {
        panic(err)
    }
    retryableClient := retryablehttp.NewClient()
    retryableClient.HTTPClient = &http.Client{Timeout: time.Second}
    retryableClient.RetryMax = 10
    retryableClient.ErrorHandler = retryablehttp.PassthroughErrorHandler

    resp, err := retryableClient.Do(retryReq)
    if err != nil {
        if resp != nil {
            // I'd like the program to enter this code block.
            fmt.Println(resp.StatusCode)
        } else {
            fmt.Println("resp is nil")
        }
        fmt.Println(err)
        return
    }
    fmt.Println(resp.StatusCode)
}
milos-matijasevic commented 2 years ago

It would be also nice to return response anyway, if there is one, instead of this https://github.com/hashicorp/go-retryablehttp/blob/991b9d0a42d13014e3689dd49a94c02be01f4237/client.go#L680