6a935d5407
1. introduce implementations of HTTPTransport and HTTPClient that apply an error wrapping policy using the constructor for a generic top-level error wrapper 2. make sure we use the implementations in point 1 when we are constructing HTTPTransport and HTTPClient 3. make sure we apply error wrapping using the constructor for a generic top-level error wrapper when reading bodies 4. acknowledge that error wrapping would be broken if we do not return the same classification _and_ operation when we wrap an already wrapped error, so fix the to code to do that 5. acknowledge that the classifiers already deal with preserving the error string and explain why this is a quirk and why we cannot remove it right now and what needs to happen to safely remove this quirk from the codebase Closes https://github.com/ooni/probe/issues/1860
122 lines
3.8 KiB
Go
122 lines
3.8 KiB
Go
package netxlite
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
)
|
|
|
|
// ErrWrapper is our error wrapper for Go errors. The key objective of
|
|
// this structure is to properly set Failure, which is also returned by
|
|
// the Error() method, to be one of the OONI failure strings.
|
|
//
|
|
// OONI failure strings are defined in the github.com/ooni/spec repo
|
|
// at https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md.
|
|
type ErrWrapper struct {
|
|
// Failure is the OONI failure string. The failure strings are
|
|
// loosely backward compatible with Measurement Kit.
|
|
//
|
|
// This is either one of the FailureXXX strings or any other
|
|
// string like `unknown_failure: ...`. The latter represents an
|
|
// error that we have not yet mapped to a failure.
|
|
Failure string
|
|
|
|
// Operation is the operation that failed.
|
|
//
|
|
// New code will always nest ErrWrapper and you need to
|
|
// walk the chain to find what happened.
|
|
//
|
|
// The following comment describes the DEPRECATED
|
|
// legacy behavior implements by internal/engine/legacy/errorsx:
|
|
//
|
|
// If possible, the Operation string
|
|
// SHOULD be a _major_ operation. Major operations are:
|
|
//
|
|
// - ResolveOperation: resolving a domain name failed
|
|
// - ConnectOperation: connecting to an IP failed
|
|
// - TLSHandshakeOperation: TLS handshaking failed
|
|
// - HTTPRoundTripOperation: other errors during round trip
|
|
//
|
|
// Because a network connection doesn't necessarily know
|
|
// what is the current major operation we also have the
|
|
// following _minor_ operations:
|
|
//
|
|
// - CloseOperation: CLOSE failed
|
|
// - ReadOperation: READ failed
|
|
// - WriteOperation: WRITE failed
|
|
//
|
|
// If an ErrWrapper referring to a major operation is wrapping
|
|
// another ErrWrapper and such ErrWrapper already refers to
|
|
// a major operation, then the new ErrWrapper should use the
|
|
// child ErrWrapper major operation. Otherwise, it should use
|
|
// its own major operation. This way, the topmost wrapper is
|
|
// supposed to refer to the major operation that failed.
|
|
Operation string
|
|
|
|
// WrappedErr is the error that we're wrapping.
|
|
WrappedErr error
|
|
}
|
|
|
|
// Error returns the OONI failure string for this error.
|
|
func (e *ErrWrapper) Error() string {
|
|
return e.Failure
|
|
}
|
|
|
|
// Unwrap allows to access the underlying error.
|
|
func (e *ErrWrapper) Unwrap() error {
|
|
return e.WrappedErr
|
|
}
|
|
|
|
// MarshalJSON converts an ErrWrapper to a JSON value.
|
|
func (e *ErrWrapper) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(e.Failure)
|
|
}
|
|
|
|
// Classifier is the type of the function that maps a Go error
|
|
// to a OONI failure string defined at
|
|
// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md.
|
|
type Classifier func(err error) string
|
|
|
|
// NewErrWrapper creates a new ErrWrapper using the given
|
|
// classifier, operation name, and underlying error.
|
|
//
|
|
// This function panics if classifier is nil, or operation
|
|
// is the empty string or error is nil.
|
|
//
|
|
// If the err argument has already been classified, the returned
|
|
// error wrapper will use the same classification string and
|
|
// failed operation of the original wrapped error.
|
|
func NewErrWrapper(c Classifier, op string, err error) *ErrWrapper {
|
|
var wrapper *ErrWrapper
|
|
if errors.As(err, &wrapper) {
|
|
return &ErrWrapper{
|
|
Failure: wrapper.Failure,
|
|
Operation: wrapper.Operation,
|
|
WrappedErr: err,
|
|
}
|
|
}
|
|
if c == nil {
|
|
panic("nil classifier")
|
|
}
|
|
if op == "" {
|
|
panic("empty op")
|
|
}
|
|
if err == nil {
|
|
panic("nil err")
|
|
}
|
|
return &ErrWrapper{
|
|
Failure: c(err),
|
|
Operation: op,
|
|
WrappedErr: err,
|
|
}
|
|
}
|
|
|
|
// NewTopLevelGenericErrWrapper wraps an error occurring at top
|
|
// level using ClassifyGenericError as classifier.
|
|
//
|
|
// If the err argument has already been classified, the returned
|
|
// error wrapper will use the same classification string and
|
|
// failed operation of the original error.
|
|
func NewTopLevelGenericErrWrapper(err error) *ErrWrapper {
|
|
return NewErrWrapper(ClassifyGenericError, TopLevelOperation, err)
|
|
}
|