Skip to content

Commit

Permalink
all: add Unwrap and Is methods to various error types
Browse files Browse the repository at this point in the history
Add Unwrap methods to types which wrap an underlying error:

  "encodinc/csv".ParseError
  "encoding/json".MarshalerError
  "net/http".transportReadFromServerError
  "net".OpError
  "net".DNSConfigError
  "net/url".Error
  "os/exec".Error
  "signal/internal/pty".PtyError
  "text/template".ExecError

Add os.ErrTemporary. A case could be made for putting this error
value in package net, since no exported error types in package os
include a Temporary method. However, syscall errors returned from
the os package do include this method.

Add Is methods to error types with a Timeout or Temporary method,
making errors.Is(err, os.Err{Timeout,Temporary}) equivalent to
testing the corresponding method:

  "context".DeadlineExceeded
  "internal/poll".TimeoutError
  "net".adrinfoErrno
  "net".OpError
  "net".DNSError
  "net/http".httpError
  "net/http".tlsHandshakeTimeoutError
  "net/pipe".timeoutError
  "net/url".Error

Updates #30322
Updates #29934

Change-Id: I409fb20c072ea39116ebfb8c7534d493483870dc
Reviewed-on: https://go-review.googlesource.com/c/go/+/170037
Run-TryBot: Damien Neil <dneil@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
  • Loading branch information
neild committed May 4, 2019
1 parent 59ea685 commit 170b8b4
Show file tree
Hide file tree
Showing 18 changed files with 208 additions and 24 deletions.
4 changes: 4 additions & 0 deletions src/context/context.go
Expand Up @@ -49,6 +49,7 @@ package context

import (
"errors"
"internal/oserror"
"internal/reflectlite"
"sync"
"time"
Expand Down Expand Up @@ -162,6 +163,9 @@ type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
func (deadlineExceededError) Is(target error) bool {
return target == oserror.ErrTimeout || target == oserror.ErrTemporary
}

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
Expand Down
5 changes: 5 additions & 0 deletions src/context/context_test.go
Expand Up @@ -5,8 +5,10 @@
package context

import (
"errors"
"fmt"
"math/rand"
"os"
"runtime"
"strings"
"sync"
Expand Down Expand Up @@ -647,4 +649,7 @@ func XTestDeadlineExceededSupportsTimeout(t testingT) {
if !i.Timeout() {
t.Fatal("wrong value for timeout")
}
if !errors.Is(DeadlineExceeded, os.ErrTimeout) {
t.Fatal("errors.Is(DeadlineExceeded, os.ErrTimeout) = false, want true")
}
}
2 changes: 2 additions & 0 deletions src/encoding/csv/reader.go
Expand Up @@ -80,6 +80,8 @@ func (e *ParseError) Error() string {
return fmt.Sprintf("parse error on line %d, column %d: %v", e.Line, e.Column, e.Err)
}

func (e *ParseError) Unwrap() error { return e.Err }

// These are the errors that can be returned in ParseError.Err.
var (
ErrTrailingComma = errors.New("extra delimiter at end of line") // Deprecated: No longer used.
Expand Down
2 changes: 2 additions & 0 deletions src/encoding/json/encode.go
Expand Up @@ -270,6 +270,8 @@ func (e *MarshalerError) Error() string {
return "json: error calling MarshalJSON for type " + e.Type.String() + ": " + e.Err.Error()
}

func (e *MarshalerError) Unwrap() error { return e.Err }

var hex = "0123456789abcdef"

// An encodeState encodes JSON into a bytes.Buffer.
Expand Down
5 changes: 3 additions & 2 deletions src/go/build/deps_test.go
Expand Up @@ -140,6 +140,7 @@ var pkgDeps = map[string][]string{
"image/color",
"image/color/palette",
"internal/fmtsort",
"internal/oserror",
"reflect",
},

Expand All @@ -166,7 +167,7 @@ var pkgDeps = map[string][]string{
"syscall/js",
},

"internal/poll": {"L0", "internal/race", "syscall", "time", "unicode/utf16", "unicode/utf8", "internal/syscall/windows"},
"internal/poll": {"L0", "internal/oserror", "internal/race", "syscall", "time", "unicode/utf16", "unicode/utf8", "internal/syscall/windows"},
"internal/testlog": {"L0"},
"os": {"L1", "os", "syscall", "time", "internal/oserror", "internal/poll", "internal/syscall/windows", "internal/syscall/unix", "internal/testlog"},
"path/filepath": {"L2", "os", "syscall", "internal/syscall/windows"},
Expand Down Expand Up @@ -249,7 +250,7 @@ var pkgDeps = map[string][]string{
"compress/gzip": {"L4", "compress/flate"},
"compress/lzw": {"L4"},
"compress/zlib": {"L4", "compress/flate"},
"context": {"errors", "internal/reflectlite", "sync", "time"},
"context": {"errors", "internal/oserror", "internal/reflectlite", "sync", "time"},
"database/sql": {"L4", "container/list", "context", "database/sql/driver", "database/sql/internal"},
"database/sql/driver": {"L4", "context", "time", "database/sql/internal"},
"debug/dwarf": {"L4"},
Expand Down
48 changes: 46 additions & 2 deletions src/internal/oserror/errors.go
Expand Up @@ -15,6 +15,50 @@ var (
ErrExist = errors.New("file already exists")
ErrNotExist = errors.New("file does not exist")
ErrClosed = errors.New("file already closed")
ErrTemporary = errors.New("temporary error")
ErrTimeout = errors.New("deadline exceeded")
ErrTemporary = temporaryError{}
ErrTimeout = timeoutError{}
)

type timeoutError struct{}

func (timeoutError) Error() string { return "deadline exceeded" }
func (timeoutError) Timeout() bool { return true }

type temporaryError struct{}

func (temporaryError) Error() string { return "temporary error" }
func (temporaryError) Temporary() bool { return true }

// IsTimeout reports whether err indicates a timeout.
func IsTimeout(err error) bool {
for err != nil {
if err == ErrTimeout {
return true
}
if x, ok := err.(interface{ Timeout() bool }); ok {
return x.Timeout()
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(ErrTimeout) {
return true
}
err = errors.Unwrap(err)
}
return false
}

// IsTemporary reports whether err indicates a temporary condition.
func IsTemporary(err error) bool {
for err != nil {
if err == ErrTemporary {
return true
}
if x, ok := err.(interface{ Temporary() bool }); ok {
return x.Temporary()
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(ErrTemporary) {
return true
}
err = errors.Unwrap(err)
}
return false
}
63 changes: 63 additions & 0 deletions src/internal/oserror/errors_test.go
@@ -0,0 +1,63 @@
package oserror_test

import (
"errors"
"fmt"
"internal/oserror"
"os"
"testing"
)

type ttError struct {
timeout bool
temporary bool
}

func (e ttError) Error() string {
return fmt.Sprintf("ttError{timeout:%v temporary:%v}", e.timeout, e.temporary)
}
func (e ttError) Timeout() bool { return e.timeout }
func (e ttError) Temporary() bool { return e.temporary }

type isError struct {
err error
}

func (e isError) Error() string { return fmt.Sprintf("isError(%v)", e.err) }
func (e isError) Is(target error) bool { return e.err == target }

func TestIsTimeout(t *testing.T) {
for _, test := range []struct {
want bool
err error
}{
{true, ttError{timeout: true}},
{true, isError{os.ErrTimeout}},
{true, os.ErrTimeout},
{true, fmt.Errorf("wrap: %w", os.ErrTimeout)},
{false, ttError{timeout: false}},
{false, errors.New("error")},
} {
if got, want := oserror.IsTimeout(test.err), test.want; got != want {
t.Errorf("IsTimeout(err) = %v, want %v\n%+v", got, want, test.err)
}
}
}

func TestIsTemporary(t *testing.T) {
for _, test := range []struct {
want bool
err error
}{
{true, ttError{temporary: true}},
{true, isError{os.ErrTemporary}},
{true, os.ErrTemporary},
{true, fmt.Errorf("wrap: %w", os.ErrTemporary)},
{false, ttError{temporary: false}},
{false, errors.New("error")},
} {
if got, want := oserror.IsTemporary(test.err), test.want; got != want {
t.Errorf("IsTemporary(err) = %v, want %v\n%+v", got, want, test.err)
}
}
}
9 changes: 8 additions & 1 deletion src/internal/poll/fd.go
Expand Up @@ -9,7 +9,10 @@
// runtime scheduler.
package poll

import "errors"
import (
"errors"
"internal/oserror"
)

// ErrNetClosing is returned when a network descriptor is used after
// it has been closed. Keep this string consistent because of issue
Expand Down Expand Up @@ -44,6 +47,10 @@ func (e *TimeoutError) Error() string { return "i/o timeout" }
func (e *TimeoutError) Timeout() bool { return true }
func (e *TimeoutError) Temporary() bool { return true }

func (e *TimeoutError) Is(target error) bool {
return target == oserror.ErrTimeout || target == oserror.ErrTemporary
}

// ErrNotPollable is returned when the file or socket is not suitable
// for event notification.
var ErrNotPollable = errors.New("not pollable")
Expand Down
11 changes: 11 additions & 0 deletions src/net/cgo_unix.go
Expand Up @@ -24,6 +24,7 @@ import "C"

import (
"context"
"os"
"syscall"
"unsafe"
)
Expand All @@ -37,6 +38,16 @@ func (eai addrinfoErrno) Error() string { return C.GoString(C.gai_strerror(C.i
func (eai addrinfoErrno) Temporary() bool { return eai == C.EAI_AGAIN }
func (eai addrinfoErrno) Timeout() bool { return false }

func (eai addrinfoErrno) Is(target error) bool {
switch target {
case os.ErrTemporary:
return eai.Temporary()
case os.ErrTimeout:
return eai.Timeout()
}
return false
}

type portLookupResult struct {
port int
err error
Expand Down
16 changes: 16 additions & 0 deletions src/net/http/transport.go
Expand Up @@ -788,6 +788,8 @@ type transportReadFromServerError struct {
err error
}

func (e transportReadFromServerError) Unwrap() error { return e.err }

func (e transportReadFromServerError) Error() string {
return fmt.Sprintf("net/http: Transport failed to read from server: %v", e.err)
}
Expand Down Expand Up @@ -2155,6 +2157,16 @@ func (e *httpError) Error() string { return e.err }
func (e *httpError) Timeout() bool { return e.timeout }
func (e *httpError) Temporary() bool { return true }

func (e *httpError) Is(target error) bool {
switch target {
case os.ErrTimeout:
return e.timeout
case os.ErrTemporary:
return true
}
return false
}

var errTimeout error = &httpError{err: "net/http: timeout awaiting response headers", timeout: true}

// errRequestCanceled is set to be identical to the one from h2 to facilitate
Expand Down Expand Up @@ -2489,6 +2501,10 @@ func (tlsHandshakeTimeoutError) Timeout() bool { return true }
func (tlsHandshakeTimeoutError) Temporary() bool { return true }
func (tlsHandshakeTimeoutError) Error() string { return "net/http: TLS handshake timeout" }

func (tlsHandshakeTimeoutError) Is(target error) bool {
return target == os.ErrTimeout || target == os.ErrTemporary
}

// fakeLocker is a sync.Locker which does nothing. It's used to guard
// test-only fields when not under test, to avoid runtime atomic
// overhead.
Expand Down
23 changes: 23 additions & 0 deletions src/net/net.go
Expand Up @@ -448,6 +448,8 @@ type OpError struct {
Err error
}

func (e *OpError) Unwrap() error { return e.Err }

func (e *OpError) Error() string {
if e == nil {
return "<nil>"
Expand Down Expand Up @@ -514,6 +516,16 @@ func (e *OpError) Temporary() bool {
return ok && t.Temporary()
}

func (e *OpError) Is(target error) bool {
switch target {
case os.ErrTemporary:
return e.Temporary()
case os.ErrTimeout:
return e.Timeout()
}
return false
}

// A ParseError is the error type of literal network address parsers.
type ParseError struct {
// Type is the type of string that was expected, such as
Expand Down Expand Up @@ -563,6 +575,7 @@ type DNSConfigError struct {
Err error
}

func (e *DNSConfigError) Unwrap() error { return e.Err }
func (e *DNSConfigError) Error() string { return "error reading DNS config: " + e.Err.Error() }
func (e *DNSConfigError) Timeout() bool { return false }
func (e *DNSConfigError) Temporary() bool { return false }
Expand Down Expand Up @@ -604,6 +617,16 @@ func (e *DNSError) Timeout() bool { return e.IsTimeout }
// error and return a DNSError for which Temporary returns false.
func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }

func (e *DNSError) Is(target error) bool {
switch target {
case os.ErrTemporary:
return e.Temporary()
case os.ErrTimeout:
return e.Timeout()
}
return false
}

type writerOnly struct {
io.Writer
}
Expand Down
5 changes: 5 additions & 0 deletions src/net/pipe.go
Expand Up @@ -6,6 +6,7 @@ package net

import (
"io"
"os"
"sync"
"time"
)
Expand Down Expand Up @@ -84,6 +85,10 @@ func (timeoutError) Error() string { return "deadline exceeded" }
func (timeoutError) Timeout() bool { return true }
func (timeoutError) Temporary() bool { return true }

func (timeoutError) Is(target error) bool {
return target == os.ErrTemporary || target == os.ErrTimeout
}

type pipeAddr struct{}

func (pipeAddr) Network() string { return "pipe" }
Expand Down
5 changes: 5 additions & 0 deletions src/net/timeout_test.go
Expand Up @@ -7,7 +7,9 @@
package net

import (
"errors"
"fmt"
"internal/oserror"
"internal/poll"
"internal/testenv"
"io"
Expand Down Expand Up @@ -88,6 +90,9 @@ func TestDialTimeout(t *testing.T) {
if nerr, ok := err.(Error); !ok || !nerr.Timeout() {
t.Fatalf("#%d: %v", i, err)
}
if !errors.Is(err, oserror.ErrTimeout) {
t.Fatalf("#%d: Dial error is not os.ErrTimeout: %v", i, err)
}
}
}
}
Expand Down

0 comments on commit 170b8b4

Please sign in to comment.