1
0
mirror of https://github.com/goproxy/goproxy.git synced 2026-06-20 09:25:06 +02:00
Files
goproxy/http.go
Aofei Sheng ca9f50b0ad refactor: use github.com/aofei/backoff for retries (#157)
- Replace the local exponential backoff helper with
  github.com/aofei/backoff so `httpGet` uses a well-tested jittered
  retry loop that still respects context cancellation
- Fix the deadline test to expect `context.DeadlineExceeded`

Signed-off-by: Aofei Sheng <aofei@aofeisheng.com>
2025-10-29 00:07:11 +08:00

141 lines
3.4 KiB
Go

package goproxy
import (
"context"
"crypto/x509"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"time"
"github.com/aofei/backoff"
)
var (
// errBadUpstream indicates an upstream is in a bad state.
errBadUpstream = errors.New("bad upstream")
// errFetchTimedOut indicates a fetch operation has timed out.
errFetchTimedOut = errors.New("fetch timed out")
)
// notExistError is like [fs.ErrNotExist] but with a custom underlying error.
//
// NOTE: Do not use [notExistError] directly, use [notExistErrorf] instead.
type notExistError struct{ err error }
// Error implements [error].
func (e *notExistError) Error() string { return e.err.Error() }
// Unwrap returns the underlying error.
func (e *notExistError) Unwrap() error { return e.err }
// Is reports whether the target is [fs.ErrNotExist].
func (notExistError) Is(target error) bool { return target == fs.ErrNotExist }
// notExistErrorf formats according to a format specifier and returns the string
// as a value that satisfies error that is equivalent to [fs.ErrNotExist].
func notExistErrorf(format string, v ...any) error {
return &notExistError{err: fmt.Errorf(format, v...)}
}
// httpGet gets the content from the given url and writes it to the dst.
func httpGet(ctx context.Context, client *http.Client, url string, dst io.Writer) error {
const (
maxAttempts = 10
backoffBase = 100 * time.Millisecond
backoffCap = time.Second
)
var lastErr error
for range backoff.Attempts(ctx, maxAttempts, backoffBase, backoffCap) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
if isRetryableHTTPClientDoError(err) {
lastErr = err
continue
}
return err
}
if resp.StatusCode == http.StatusOK {
if dst != nil {
_, err = io.Copy(dst, resp.Body)
}
resp.Body.Close()
return err
}
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return err
}
switch resp.StatusCode {
case http.StatusBadRequest,
http.StatusNotFound,
http.StatusGone:
return notExistErrorf("%s", respBody)
case http.StatusTooManyRequests,
http.StatusInternalServerError,
http.StatusBadGateway,
http.StatusServiceUnavailable:
lastErr = errBadUpstream
case http.StatusGatewayTimeout:
lastErr = errFetchTimedOut
default:
return fmt.Errorf("GET %s: %s: %s", resp.Request.URL.Redacted(), resp.Status, respBody)
}
}
if err := ctx.Err(); err != nil {
return err
}
return lastErr
}
// httpGetTemp is like [httpGet] but writes the content to a new temporary file
// in tempDir.
func httpGetTemp(ctx context.Context, client *http.Client, url, tempDir string) (tempFile string, err error) {
f, err := os.CreateTemp(tempDir, "")
if err != nil {
return "", err
}
defer func() {
if err != nil {
os.Remove(f.Name())
}
}()
if err := httpGet(ctx, client, url, f); err != nil {
f.Close()
return "", err
}
return f.Name(), f.Close()
}
// isRetryableHTTPClientDoError reports whether the err is a retryable error
// returned by [http.Client.Do].
func isRetryableHTTPClientDoError(err error) bool {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
if ue, ok := err.(*url.Error); ok {
e := ue.Unwrap()
switch e.(type) {
case x509.UnknownAuthorityError:
return false
}
if errors.Is(e, http.ErrSchemeMismatch) {
return false
}
}
return true
}