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, 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 wantErr error wantNotErr error }{ { name: "connection refused", dial: func(context.CancelFunc) error { return syscall.ECONNREFUSED }, wantErr: ErrUpstreamRequestFailed, wantNotErr: context.Canceled, }, { name: "timed out while connecting", dial: func(context.CancelFunc) error { return os.ErrDeadlineExceeded }, wantErr: ErrUpstreamTimeout, wantNotErr: ErrUpstreamRequestFailed, }, { name: "cancelled while connecting", dial: func(cancel context.CancelFunc) error { cancel() return context.Canceled }, wantErr: 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) if !errors.Is(err, tt.wantErr) { t.Errorf("Fetch() error = %v, want %v", err, tt.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) }) }