fix: enable errorlint in api, client and pkg

Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
This commit is contained in:
Matthieu MOREL 2024-10-13 19:39:18 +02:00
parent d6412f468e
commit b281a3c4fa
22 changed files with 69 additions and 52 deletions

View File

@ -15,6 +15,7 @@
package rpctypes package rpctypes
import ( import (
"errors"
"testing" "testing"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@ -24,19 +25,20 @@ import (
func TestConvert(t *testing.T) { func TestConvert(t *testing.T) {
e1 := status.Error(codes.InvalidArgument, "etcdserver: key is not provided") e1 := status.Error(codes.InvalidArgument, "etcdserver: key is not provided")
e2 := ErrGRPCEmptyKey e2 := ErrGRPCEmptyKey
e3 := ErrEmptyKey var e3 EtcdError
errors.As(ErrEmptyKey, &e3)
if e1.Error() != e2.Error() { if e1.Error() != e2.Error() {
t.Fatalf("expected %q == %q", e1.Error(), e2.Error()) t.Fatalf("expected %q == %q", e1.Error(), e2.Error())
} }
if ev1, ok := status.FromError(e1); ok && ev1.Code() != e3.(EtcdError).Code() { if ev1, ok := status.FromError(e1); ok && ev1.Code() != e3.Code() {
t.Fatalf("expected them to be equal, got %v / %v", ev1.Code(), e3.(EtcdError).Code()) t.Fatalf("expected them to be equal, got %v / %v", ev1.Code(), e3.Code())
} }
if e1.Error() == e3.Error() { if e1.Error() == e3.Error() {
t.Fatalf("expected %q != %q", e1.Error(), e3.Error()) t.Fatalf("expected %q != %q", e1.Error(), e3.Error())
} }
if ev2, ok := status.FromError(e2); ok && ev2.Code() != e3.(EtcdError).Code() { if ev2, ok := status.FromError(e2); ok && ev2.Code() != e3.Code() {
t.Fatalf("expected them to be equal, got %v / %v", ev2.Code(), e3.(EtcdError).Code()) t.Fatalf("expected them to be equal, got %v / %v", ev2.Code(), e3.Code())
} }
} }

View File

@ -17,6 +17,7 @@
package fileutil package fileutil
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -58,13 +59,13 @@ func TryLockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
func ofdTryLockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) { func ofdTryLockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
f, err := os.OpenFile(path, flag, perm) f, err := os.OpenFile(path, flag, perm)
if err != nil { if err != nil {
return nil, fmt.Errorf("ofdTryLockFile failed to open %q (%v)", path, err) return nil, fmt.Errorf("ofdTryLockFile failed to open %q (%w)", path, err)
} }
flock := wrlck flock := wrlck
if err = syscall.FcntlFlock(f.Fd(), unix.F_OFD_SETLK, &flock); err != nil { if err = syscall.FcntlFlock(f.Fd(), unix.F_OFD_SETLK, &flock); err != nil {
f.Close() f.Close()
if err == syscall.EWOULDBLOCK { if errors.Is(err, syscall.EWOULDBLOCK) {
err = ErrLocked err = ErrLocked
} }
return nil, err return nil, err
@ -79,7 +80,7 @@ func LockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
func ofdLockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) { func ofdLockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
f, err := os.OpenFile(path, flag, perm) f, err := os.OpenFile(path, flag, perm)
if err != nil { if err != nil {
return nil, fmt.Errorf("ofdLockFile failed to open %q (%v)", path, err) return nil, fmt.Errorf("ofdLockFile failed to open %q (%w)", path, err)
} }
flock := wrlck flock := wrlck

View File

@ -17,6 +17,7 @@
package fileutil package fileutil
import ( import (
"errors"
"os" "os"
"syscall" "syscall"
) )
@ -25,10 +26,10 @@ func preallocExtend(f *os.File, sizeInBytes int64) error {
// use mode = 0 to change size // use mode = 0 to change size
err := syscall.Fallocate(int(f.Fd()), 0, 0, sizeInBytes) err := syscall.Fallocate(int(f.Fd()), 0, 0, sizeInBytes)
if err != nil { if err != nil {
errno, ok := err.(syscall.Errno) var errno syscall.Errno
// not supported; fallback // not supported; fallback
// fallocate EINTRs frequently in some environments; fallback // fallocate EINTRs frequently in some environments; fallback
if ok && (errno == syscall.ENOTSUP || errno == syscall.EINTR) { if errors.As(err, &errno) && (errno == syscall.ENOTSUP || errno == syscall.EINTR) {
return preallocExtendTrunc(f, sizeInBytes) return preallocExtendTrunc(f, sizeInBytes)
} }
} }
@ -39,9 +40,9 @@ func preallocFixed(f *os.File, sizeInBytes int64) error {
// use mode = 1 to keep size; see FALLOC_FL_KEEP_SIZE // use mode = 1 to keep size; see FALLOC_FL_KEEP_SIZE
err := syscall.Fallocate(int(f.Fd()), 1, 0, sizeInBytes) err := syscall.Fallocate(int(f.Fd()), 1, 0, sizeInBytes)
if err != nil { if err != nil {
errno, ok := err.(syscall.Errno) var errno syscall.Errno
// treat not supported as nil error // treat not supported as nil error
if ok && errno == syscall.ENOTSUP { if errors.As(err, &errno) && errno == syscall.ENOTSUP {
return nil return nil
} }
} }

View File

@ -87,7 +87,7 @@ func GetCluster(serviceScheme, service, name, dns string, apurls types.URLs) ([]
err := updateNodeMap(service, serviceScheme) err := updateNodeMap(service, serviceScheme)
if err != nil { if err != nil {
return nil, fmt.Errorf("error querying DNS SRV records for _%s %s", service, err) return nil, fmt.Errorf("error querying DNS SRV records for _%s %w", service, err)
} }
return stringParts, nil return stringParts, nil
} }
@ -123,7 +123,7 @@ func GetClient(service, domain string, serviceName string) (*SRVClients, error)
errHTTP := updateURLs(GetSRVService(service, serviceName, "http"), "http") errHTTP := updateURLs(GetSRVService(service, serviceName, "http"), "http")
if errHTTPS != nil && errHTTP != nil { if errHTTPS != nil && errHTTP != nil {
return nil, fmt.Errorf("dns lookup errors: %s and %s", errHTTPS, errHTTP) return nil, fmt.Errorf("dns lookup errors: %w and %w", errHTTPS, errHTTP)
} }
endpoints := make([]string, len(urls)) endpoints := make([]string, len(urls))

View File

@ -15,6 +15,7 @@
package transport package transport
import ( import (
"errors"
"net" "net"
"testing" "testing"
"time" "time"
@ -57,7 +58,8 @@ func TestReadWriteTimeoutDialer(t *testing.T) {
t.Fatal("wait timeout") t.Fatal("wait timeout")
} }
if operr, ok := err.(*net.OpError); !ok || operr.Op != "write" || !operr.Timeout() { var operr *net.OpError
if !errors.As(err, &operr) || operr.Op != "write" || !operr.Timeout() {
t.Errorf("err = %v, want write i/o timeout error", err) t.Errorf("err = %v, want write i/o timeout error", err)
} }
@ -77,7 +79,7 @@ func TestReadWriteTimeoutDialer(t *testing.T) {
t.Fatal("wait timeout") t.Fatal("wait timeout")
} }
if operr, ok := err.(*net.OpError); !ok || operr.Op != "read" || !operr.Timeout() { if !errors.As(err, &operr) || operr.Op != "read" || !operr.Timeout() {
t.Errorf("err = %v, want read i/o timeout error", err) t.Errorf("err = %v, want read i/o timeout error", err)
} }
} }

View File

@ -15,6 +15,7 @@
package transport package transport
import ( import (
"errors"
"net" "net"
"testing" "testing"
"time" "time"
@ -82,7 +83,8 @@ func TestWriteReadTimeoutListener(t *testing.T) {
t.Fatal("wait timeout") t.Fatal("wait timeout")
} }
if operr, ok := err.(*net.OpError); !ok || operr.Op != "write" || !operr.Timeout() { var operr *net.OpError
if !errors.As(err, &operr) || operr.Op != "write" || !operr.Timeout() {
t.Errorf("err = %v, want write i/o timeout error", err) t.Errorf("err = %v, want write i/o timeout error", err)
} }
writerStopCh <- struct{}{} writerStopCh <- struct{}{}
@ -109,7 +111,7 @@ func TestWriteReadTimeoutListener(t *testing.T) {
t.Fatal("wait timeout") t.Fatal("wait timeout")
} }
if operr, ok := err.(*net.OpError); !ok || operr.Op != "read" || !operr.Timeout() { if !errors.As(err, &operr) || operr.Op != "read" || !operr.Timeout() {
t.Errorf("err = %v, want read i/o timeout error", err) t.Errorf("err = %v, want read i/o timeout error", err)
} }
readerStopCh <- struct{}{} readerStopCh <- struct{}{}

View File

@ -294,7 +294,7 @@ func (c *Client) getToken(ctx context.Context) error {
resp, err := c.Auth.Authenticate(ctx, c.Username, c.Password) resp, err := c.Auth.Authenticate(ctx, c.Username, c.Password)
if err != nil { if err != nil {
if err == rpctypes.ErrAuthNotEnabled { if errors.Is(err, rpctypes.ErrAuthNotEnabled) {
c.authTokenBundle.UpdateAuthToken("") c.authTokenBundle.UpdateAuthToken("")
return nil return nil
} }
@ -605,7 +605,8 @@ func ContextError(ctx context.Context, err error) error {
return nil return nil
} }
err = rpctypes.Error(err) err = rpctypes.Error(err)
if _, ok := err.(rpctypes.EtcdError); ok { var serverErr rpctypes.EtcdError
if errors.As(err, &serverErr) {
return err return err
} }
if ev, ok := status.FromError(err); ok { if ev, ok := status.FromError(err); ok {
@ -627,7 +628,7 @@ func canceledByCaller(stopCtx context.Context, err error) bool {
return false return false
} }
return err == context.Canceled || err == context.DeadlineExceeded return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
} }
// IsConnCanceled returns true, if error is from a closed gRPC connection. // IsConnCanceled returns true, if error is from a closed gRPC connection.
@ -645,7 +646,7 @@ func IsConnCanceled(err error) bool {
} }
// >= gRPC v1.10.x // >= gRPC v1.10.x
if err == context.Canceled { if errors.Is(err, context.Canceled) {
return true return true
} }

View File

@ -430,8 +430,8 @@ func TestClientRejectOldCluster(t *testing.T) {
}, },
} }
if err := c.checkVersion(); err != tt.expectedError { if err := c.checkVersion(); !errors.Is(err, tt.expectedError) {
t.Errorf("heckVersion err:%v", err) t.Errorf("checkVersion err:%v", err)
} }
}) })
} }

View File

@ -16,6 +16,7 @@ package recipe
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -51,7 +52,7 @@ func newUniqueKV(kv v3.KV, prefix string, val string) (*RemoteKV, error) {
if err == nil { if err == nil {
return &RemoteKV{kv, newKey, rev, val}, nil return &RemoteKV{kv, newKey, rev, val}, nil
} }
if err != ErrKeyExists { if !errors.Is(err, ErrKeyExists) {
return nil, err return nil, err
} }
} }
@ -155,7 +156,7 @@ func newUniqueEphemeralKV(s *concurrency.Session, prefix, val string) (ek *Ephem
for { for {
newKey := fmt.Sprintf("%s/%v", prefix, time.Now().UnixNano()) newKey := fmt.Sprintf("%s/%v", prefix, time.Now().UnixNano())
ek, err = newEphemeralKV(s, newKey, val) ek, err = newEphemeralKV(s, newKey, val)
if err == nil || err != ErrKeyExists { if err == nil || !errors.Is(err, ErrKeyExists) {
break break
} }
} }

View File

@ -16,6 +16,7 @@ package clientv3
import ( import (
"context" "context"
"errors"
"sync" "sync"
"time" "time"
@ -464,7 +465,7 @@ func (l *lessor) recvKeepAliveLoop() (gerr error) {
return err return err
} }
if ContextError(l.stopCtx, err) == rpctypes.ErrNoLeader { if errors.Is(ContextError(l.stopCtx, err), rpctypes.ErrNoLeader) {
l.closeRequireLeader() l.closeRequireLeader()
} }
break break

View File

@ -16,6 +16,7 @@ package leasing
import ( import (
"context" "context"
"errors"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -282,7 +283,8 @@ func (lkv *leasingKV) acquire(ctx context.Context, key string, op v3.Op) (*v3.Tx
return resp, nil return resp, nil
} }
// retry if transient error // retry if transient error
if _, ok := err.(rpctypes.EtcdError); ok { var serverErr rpctypes.EtcdError
if errors.As(err, &serverErr) {
return nil, err return nil, err
} }
if ev, ok := status.FromError(err); ok && ev.Code() != codes.Unavailable { if ev, ok := status.FromError(err); ok && ev.Code() != codes.Unavailable {

View File

@ -117,7 +117,7 @@ func NewMaintenance(c *Client) Maintenance {
dial: func(endpoint string) (pb.MaintenanceClient, func(), error) { dial: func(endpoint string) (pb.MaintenanceClient, func(), error) {
conn, err := c.Dial(endpoint) conn, err := c.Dial(endpoint)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to dial endpoint %s with maintenance client: %v", endpoint, err) return nil, nil, fmt.Errorf("failed to dial endpoint %s with maintenance client: %w", endpoint, err)
} }
cancel := func() { conn.Close() } cancel := func() { conn.Close() }
@ -297,8 +297,8 @@ func (m *maintenance) Snapshot(ctx context.Context) (io.ReadCloser, error) {
} }
func (m *maintenance) logAndCloseWithError(err error, pw *io.PipeWriter) { func (m *maintenance) logAndCloseWithError(err error, pw *io.PipeWriter) {
switch err { switch {
case io.EOF: case errors.Is(err, io.EOF):
m.lg.Info("completed snapshot read; closing") m.lg.Info("completed snapshot read; closing")
default: default:
m.lg.Warn("failed to receive from snapshot stream; closing", zap.Error(err)) m.lg.Warn("failed to receive from snapshot stream; closing", zap.Error(err))

View File

@ -85,12 +85,12 @@ func startMockServersUnix(count int) (ms *MockServers, err error) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
f, err := os.CreateTemp(dir, "etcd-unix-so-") f, err := os.CreateTemp(dir, "etcd-unix-so-")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to allocate temp file for unix socket: %v", err) return nil, fmt.Errorf("failed to allocate temp file for unix socket: %w", err)
} }
fn := f.Name() fn := f.Name()
err = os.Remove(fn) err = os.Remove(fn)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to remove temp file before creating unix socket: %v", err) return nil, fmt.Errorf("failed to remove temp file before creating unix socket: %w", err)
} }
addrs = append(addrs, fn) addrs = append(addrs, fn)
} }
@ -110,7 +110,7 @@ func startMockServers(network string, addrs []string) (ms *MockServers, err erro
for idx, addr := range addrs { for idx, addr := range addrs {
ln, err := net.Listen(network, addr) ln, err := net.Listen(network, addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to listen %v", err) return nil, fmt.Errorf("failed to listen %w", err)
} }
ms.Servers[idx] = &MockServer{ln: ln, Network: network, Address: ln.Addr().String()} ms.Servers[idx] = &MockServer{ln: ln, Network: network, Address: ln.Addr().String()}
ms.StartAt(idx) ms.StartAt(idx)
@ -126,7 +126,7 @@ func (ms *MockServers) StartAt(idx int) (err error) {
if ms.Servers[idx].ln == nil { if ms.Servers[idx].ln == nil {
ms.Servers[idx].ln, err = net.Listen(ms.Servers[idx].Network, ms.Servers[idx].Address) ms.Servers[idx].ln, err = net.Listen(ms.Servers[idx].Network, ms.Servers[idx].Address)
if err != nil { if err != nil {
return fmt.Errorf("failed to listen %v", err) return fmt.Errorf("failed to listen %w", err)
} }
} }

View File

@ -16,6 +16,7 @@ package clientv3
import ( import (
"context" "context"
"errors"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@ -52,7 +53,8 @@ func (rp retryPolicy) String() string {
// handle itself even with retries. // handle itself even with retries.
func isSafeRetryImmutableRPC(err error) bool { func isSafeRetryImmutableRPC(err error) bool {
eErr := rpctypes.Error(err) eErr := rpctypes.Error(err)
if serverErr, ok := eErr.(rpctypes.EtcdError); ok && serverErr.Code() != codes.Unavailable { var serverErr rpctypes.EtcdError
if errors.As(eErr, &serverErr) && serverErr.Code() != codes.Unavailable {
// interrupted by non-transient server-side or gRPC-side error // interrupted by non-transient server-side or gRPC-side error
// client cannot handle itself (e.g. rpctypes.ErrCompacted) // client cannot handle itself (e.g. rpctypes.ErrCompacted)
return false return false

View File

@ -349,10 +349,10 @@ func isContextError(err error) bool {
} }
func contextErrToGRPCErr(err error) error { func contextErrToGRPCErr(err error) error {
switch err { switch {
case context.DeadlineExceeded: case errors.Is(err, context.DeadlineExceeded):
return status.Errorf(codes.DeadlineExceeded, err.Error()) return status.Errorf(codes.DeadlineExceeded, err.Error())
case context.Canceled: case errors.Is(err, context.Canceled):
return status.Errorf(codes.Canceled, err.Error()) return status.Errorf(codes.Canceled, err.Error())
default: default:
return status.Errorf(codes.Unknown, err.Error()) return status.Errorf(codes.Unknown, err.Error())

View File

@ -62,7 +62,7 @@ func SaveWithVersion(ctx context.Context, lg *zap.Logger, cfg clientv3.Config, d
var f *os.File var f *os.File
f, err = os.OpenFile(partpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileutil.PrivateFileMode) f, err = os.OpenFile(partpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileutil.PrivateFileMode)
if err != nil { if err != nil {
return "", fmt.Errorf("could not open %s (%v)", partpath, err) return "", fmt.Errorf("could not open %s (%w)", partpath, err)
} }
lg.Info("created temporary db file", zap.String("path", partpath)) lg.Info("created temporary db file", zap.String("path", partpath))
@ -95,7 +95,7 @@ func SaveWithVersion(ctx context.Context, lg *zap.Logger, cfg clientv3.Config, d
) )
if err = os.Rename(partpath, dbPath); err != nil { if err = os.Rename(partpath, dbPath); err != nil {
return resp.Version, fmt.Errorf("could not rename %s to %s (%v)", partpath, dbPath, err) return resp.Version, fmt.Errorf("could not rename %s to %s (%w)", partpath, dbPath, err)
} }
lg.Info("saved", zap.String("path", dbPath)) lg.Info("saved", zap.String("path", dbPath))
return resp.Version, nil return resp.Version, nil

View File

@ -395,7 +395,7 @@ func (w *watcher) Close() (err error) {
} }
} }
// Consider context.Canceled as a successful close // Consider context.Canceled as a successful close
if err == context.Canceled { if errors.Is(err, context.Canceled) {
err = nil err = nil
} }
return err return err
@ -653,7 +653,7 @@ func (w *watchGRPCStream) run() {
// watch client failed on Recv; spawn another if possible // watch client failed on Recv; spawn another if possible
case err := <-w.errc: case err := <-w.errc:
if isHaltErr(w.ctx, err) || ContextError(w.ctx, err) == v3rpc.ErrNoLeader { if isHaltErr(w.ctx, err) || errors.Is(ContextError(w.ctx, err), v3rpc.ErrNoLeader) {
closeErr = err closeErr = err
return return
} }

View File

@ -201,7 +201,7 @@ func (f *featureGate) Set(value string) error {
v := strings.TrimSpace(arr[1]) v := strings.TrimSpace(arr[1])
boolValue, err := strconv.ParseBool(v) boolValue, err := strconv.ParseBool(v)
if err != nil { if err != nil {
return fmt.Errorf("invalid value of %s=%s, err: %v", k, v, err) return fmt.Errorf("invalid value of %s=%s, err: %w", k, v, err)
} }
m[k] = boolValue m[k] = boolValue
} }

View File

@ -108,7 +108,7 @@ func setFlagFromEnv(lg *zap.Logger, fs flagSetter, prefix, fname string, usedEnv
if val != "" { if val != "" {
usedEnvKey[key] = true usedEnvKey[key] = true
if serr := fs.Set(fname, val); serr != nil { if serr := fs.Set(fname, val); serr != nil {
return fmt.Errorf("invalid value %q for %s: %v", val, key, serr) return fmt.Errorf("invalid value %q for %s: %w", val, key, serr)
} }
if log && lg != nil { if log && lg != nil {
lg.Info( lg.Info(

View File

@ -63,7 +63,7 @@ func (ss *StubServer) Start(sopts []grpc.ServerOption, dopts ...grpc.DialOption)
lis, err := net.Listen(ss.Network, ss.Address) lis, err := net.Listen(ss.Network, ss.Address)
if err != nil { if err != nil {
return fmt.Errorf("net.Listen(%q, %q) = %v", ss.Network, ss.Address, err) return fmt.Errorf("net.Listen(%q, %q) = %w", ss.Network, ss.Address, err)
} }
ss.Address = lis.Addr().String() ss.Address = lis.Addr().String()
ss.cleanups = append(ss.cleanups, func() { lis.Close() }) ss.cleanups = append(ss.cleanups, func() { lis.Close() })

View File

@ -16,6 +16,7 @@ package ioutil
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"testing" "testing"
) )
@ -28,7 +29,7 @@ func (rc *readerNilCloser) Close() error { return nil }
func TestExactReadCloserExpectEOF(t *testing.T) { func TestExactReadCloserExpectEOF(t *testing.T) {
buf := bytes.NewBuffer(make([]byte, 10)) buf := bytes.NewBuffer(make([]byte, 10))
rc := NewExactReadCloser(&readerNilCloser{buf}, 1) rc := NewExactReadCloser(&readerNilCloser{buf}, 1)
if _, err := rc.Read(make([]byte, 10)); err != ErrExpectEOF { if _, err := rc.Read(make([]byte, 10)); !errors.Is(err, ErrExpectEOF) {
t.Fatalf("expected %v, got %v", ErrExpectEOF, err) t.Fatalf("expected %v, got %v", ErrExpectEOF, err)
} }
} }
@ -40,7 +41,7 @@ func TestExactReadCloserShort(t *testing.T) {
if _, err := rc.Read(make([]byte, 10)); err != nil { if _, err := rc.Read(make([]byte, 10)); err != nil {
t.Fatalf("Read expected nil err, got %v", err) t.Fatalf("Read expected nil err, got %v", err)
} }
if err := rc.Close(); err != ErrShortRead { if err := rc.Close(); !errors.Is(err, ErrShortRead) {
t.Fatalf("Close expected %v, got %v", ErrShortRead, err) t.Fatalf("Close expected %v, got %v", ErrShortRead, err)
} }
} }

View File

@ -16,6 +16,7 @@ package netutil
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
@ -70,14 +71,14 @@ func resolveTCPAddrs(ctx context.Context, lg *zap.Logger, urls [][]url.URL) ([][
for i, u := range us { for i, u := range us {
nu, err := url.Parse(u.String()) nu, err := url.Parse(u.String())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse %q (%v)", u.String(), err) return nil, fmt.Errorf("failed to parse %q (%w)", u.String(), err)
} }
nus[i] = *nu nus[i] = *nu
} }
for i, u := range nus { for i, u := range nus {
h, err := resolveURL(ctx, lg, u) h, err := resolveURL(ctx, lg, u)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve %q (%v)", u.String(), err) return nil, fmt.Errorf("failed to resolve %q (%w)", u.String(), err)
} }
if h != "" { if h != "" {
nus[i].Host = h nus[i].Host = h
@ -217,6 +218,6 @@ func stringsToURLs(us []string) ([]url.URL, error) {
} }
func IsNetworkTimeoutError(err error) bool { func IsNetworkTimeoutError(err error) bool {
nerr, ok := err.(net.Error) var nerr net.Error
return ok && nerr.Timeout() return errors.As(err, &nerr) && nerr.Timeout()
} }