check / check (push) Canceled after 0s
The image routes answered 500 and logged at error level when the upstream host had no address, when the fetch failed before the upstream answered, or when the fetcher refused a redirect. Such a failure is now ErrInvalidHost or ErrUpstreamRequestFailed, which respondImageError answers 502 "upstream error" and logs at warn level. A TLS handshake, an address lookup or a connection that times out is now ErrUpstreamTimeout and answers 504; its log line keeps the cause. A cancelled fetch or address lookup is neither ErrUpstreamRequestFailed nor ErrInvalidHost. README Routes lists these answers. Model: opus-5-5
214 lines
6.0 KiB
Go
214 lines
6.0 KiB
Go
package httpfetcher
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestFetchFailedBeforeAnswer checks the error of a fetch whose connection
|
|
// fails:
|
|
// - refused: ErrUpstreamRequestFailed.
|
|
// - timed out: ErrUpstreamTimeout, still matching the dial's error, even for
|
|
// a timeout error that does not match context.DeadlineExceeded, such as
|
|
// the TLS handshake timeout.
|
|
// - cancelled by its caller: context.Canceled, not ErrUpstreamRequestFailed.
|
|
func TestFetchFailedBeforeAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
dial func(cancel context.CancelFunc) error
|
|
wantErrs []error
|
|
wantNotErr error
|
|
}{
|
|
{
|
|
name: "connection refused",
|
|
dial: func(context.CancelFunc) error {
|
|
return syscall.ECONNREFUSED
|
|
},
|
|
wantErrs: []error{ErrUpstreamRequestFailed},
|
|
wantNotErr: context.Canceled,
|
|
},
|
|
{
|
|
name: "timed out while connecting",
|
|
dial: func(context.CancelFunc) error {
|
|
return os.ErrDeadlineExceeded
|
|
},
|
|
wantErrs: []error{ErrUpstreamTimeout, os.ErrDeadlineExceeded},
|
|
wantNotErr: ErrUpstreamRequestFailed,
|
|
},
|
|
{
|
|
name: "cancelled while connecting",
|
|
dial: func(cancel context.CancelFunc) error {
|
|
cancel()
|
|
|
|
return context.Canceled
|
|
},
|
|
wantErrs: []error{context.Canceled},
|
|
wantNotErr: ErrUpstreamRequestFailed,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithCancel(testContext(t))
|
|
defer cancel()
|
|
|
|
f := New(&Config{
|
|
Timeout: 30 * time.Second,
|
|
MaxResponseSize: 50 << 20,
|
|
AllowHTTP: true,
|
|
MaxConnectionsPerHost: 20,
|
|
MaxConnections: 64,
|
|
DialContext: func(context.Context, string, string) (net.Conn, error) {
|
|
return nil, tt.dial(cancel)
|
|
},
|
|
})
|
|
|
|
_, err := f.Fetch(ctx, upstreamSource("/image"), "", "")
|
|
t.Logf("Fetch() error: %v", err)
|
|
|
|
for _, wantErr := range tt.wantErrs {
|
|
if !errors.Is(err, wantErr) {
|
|
t.Errorf("Fetch() error = %v, want %v", err, wantErr)
|
|
}
|
|
}
|
|
|
|
if errors.Is(err, tt.wantNotErr) {
|
|
t.Errorf("Fetch() error = %v, must not be %v", err, tt.wantNotErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFetchAddressLookupTimedOut checks that a fetch whose address lookup
|
|
// times out fails with ErrUpstreamTimeout, both for the lookup of the source
|
|
// host and for the lookup while connecting. The error must match neither
|
|
// ErrInvalidHost nor ErrUpstreamRequestFailed, which the image routes answer
|
|
// 502. A context whose deadline has passed makes the lookup time out.
|
|
func TestFetchAddressLookupTimedOut(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checkTimedOut := func(t *testing.T, err error) {
|
|
t.Helper()
|
|
t.Logf("Fetch() error: %v", err)
|
|
|
|
if !errors.Is(err, ErrUpstreamTimeout) {
|
|
t.Errorf("Fetch() error = %v, want ErrUpstreamTimeout", err)
|
|
}
|
|
|
|
if errors.Is(err, ErrInvalidHost) || errors.Is(err, ErrUpstreamRequestFailed) {
|
|
t.Errorf("Fetch() error = %v, must match neither ErrInvalidHost "+
|
|
"nor ErrUpstreamRequestFailed", err)
|
|
}
|
|
}
|
|
|
|
t.Run("lookup of the source host", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithTimeout(testContext(t), 0)
|
|
defer cancel()
|
|
|
|
f := New(&Config{
|
|
Timeout: 30 * time.Second,
|
|
MaxResponseSize: 50 << 20,
|
|
MaxConnectionsPerHost: 20,
|
|
MaxConnections: 64,
|
|
})
|
|
|
|
_, err := f.Fetch(ctx, imageSourceOn("cdn.example.com"), "", "")
|
|
checkTimedOut(t, err)
|
|
})
|
|
|
|
t.Run("lookup while connecting", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// The source host is an address, which validateURL does not look up;
|
|
// the dialer looks up a name
|
|
f := New(&Config{
|
|
Timeout: 30 * time.Second,
|
|
MaxResponseSize: 50 << 20,
|
|
MaxConnectionsPerHost: 20,
|
|
MaxConnections: 64,
|
|
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 0)
|
|
defer cancel()
|
|
|
|
return dialSSRFSafe(ctx, network, "img.example.com:443", nil)
|
|
},
|
|
})
|
|
|
|
_, err := f.Fetch(testContext(t), upstreamSource("/image"), "", "")
|
|
checkTimedOut(t, err)
|
|
})
|
|
}
|
|
|
|
// TestFetchConnectionTimedOut checks that a fetch whose connection in
|
|
// dialSSRFSafe times out fails with ErrUpstreamTimeout, which the image
|
|
// routes answer 504. The error must match neither ErrUpstreamRequestFailed
|
|
// nor ErrInvalidHost, which they answer 502. A context whose deadline has
|
|
// passed makes the connection time out without touching the network.
|
|
func TestFetchConnectionTimedOut(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := New(&Config{
|
|
Timeout: 30 * time.Second,
|
|
MaxResponseSize: 50 << 20,
|
|
MaxConnectionsPerHost: 20,
|
|
MaxConnections: 64,
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 0)
|
|
defer cancel()
|
|
|
|
return dialSSRFSafe(ctx, network, addr, nil)
|
|
},
|
|
})
|
|
|
|
_, err := f.Fetch(testContext(t), upstreamSource("/image"), "", "")
|
|
t.Logf("Fetch() error: %v", err)
|
|
|
|
if !errors.Is(err, ErrUpstreamTimeout) {
|
|
t.Errorf("Fetch() error = %v, want ErrUpstreamTimeout", err)
|
|
}
|
|
|
|
if errors.Is(err, ErrUpstreamRequestFailed) || errors.Is(err, ErrInvalidHost) {
|
|
t.Errorf("Fetch() error = %v, must match neither "+
|
|
"ErrUpstreamRequestFailed nor ErrInvalidHost", err)
|
|
}
|
|
}
|
|
|
|
// TestFetchAddressLookupCancelled checks that a fetch whose address lookup
|
|
// its caller cancelled fails with an error matching context.Canceled, not
|
|
// ErrInvalidHost, which the image routes answer 502.
|
|
func TestFetchAddressLookupCancelled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithCancel(testContext(t))
|
|
cancel()
|
|
|
|
f := New(&Config{
|
|
Timeout: 30 * time.Second,
|
|
MaxResponseSize: 50 << 20,
|
|
MaxConnectionsPerHost: 20,
|
|
MaxConnections: 64,
|
|
})
|
|
|
|
_, err := f.Fetch(ctx, imageSourceOn("cdn.example.com"), "", "")
|
|
t.Logf("Fetch() error: %v", err)
|
|
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Errorf("Fetch() error = %v, want context.Canceled", err)
|
|
}
|
|
|
|
if errors.Is(err, ErrInvalidHost) {
|
|
t.Errorf("Fetch() error = %v, must not be ErrInvalidHost", err)
|
|
}
|
|
}
|