Files
rclone/fs/fshttp/fault.go
T
Nick Craig-Wood 8d8b82ba6f fshttp: add a fault injector for testing transient HTTP failures
Add fshttp.SetFaultInjector, which installs a function consulted by
every Transport before a request is sent. The injector can synthesise
an error status code or a transport error for chosen requests. The
request body is drained and closed as a real round trip would, but
nothing reaches the server.

This lets the integration tests check that backends cope with a
transient failure part way through an upload - in particular that a
retry re-sends the same data rather than an already consumed or
freed buffer - without needing a fake server for each backend.
2026-09-01 14:21:52 +01:00

68 lines
1.8 KiB
Go

package fshttp
import (
"fmt"
"io"
"net/http"
"sync"
)
// FaultInjector decides whether to fail req instead of sending it.
//
// It returns a non-zero HTTP status code to synthesise an error
// response, a non-nil error to fail the request at the transport, or
// (0, nil) to send the request normally. When a fault is injected the
// request body is drained and closed as it would be by a real round
// trip, but nothing is sent to the server.
type FaultInjector func(req *http.Request) (statusCode int, err error)
var (
faultInjectorMu sync.RWMutex
faultInjector FaultInjector
)
// SetFaultInjector installs f as the fault injector for every Transport,
// or removes it if f is nil.
//
// This is intended for tests which need to check how callers cope with
// transient HTTP failures, such as whether an upload is retried
// correctly after a 5xx.
func SetFaultInjector(f FaultInjector) {
faultInjectorMu.Lock()
defer faultInjectorMu.Unlock()
faultInjector = f
}
// injectFault consults the fault injector and returns the synthesised
// response or error for req, or (nil, nil) if it should be sent.
func injectFault(req *http.Request) (*http.Response, error) {
faultInjectorMu.RLock()
f := faultInjector
faultInjectorMu.RUnlock()
if f == nil {
return nil, nil
}
statusCode, err := f(req)
if statusCode == 0 && err == nil {
return nil, nil
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
_ = req.Body.Close()
}
if err != nil {
return nil, err
}
return &http.Response{
Status: fmt.Sprintf("%d %s", statusCode, http.StatusText(statusCode)),
StatusCode: statusCode,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Body: http.NoBody,
ContentLength: 0,
Request: req,
}, nil
}