pkg/api/apierror/error.go (129 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package apierror import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "reflect" "unsafe" "github.com/go-openapi/runtime" "github.com/elastic/cloud-sdk-go/pkg/models" "github.com/elastic/cloud-sdk-go/pkg/multierror" ) const ( // ErrTimedOutMsg is returned when the error is context.DeadlineExceeded. ErrTimedOutMsg = "operation timed out" errPrefix = "api error" httpRetryWith = 449 ) type basicFailedReply interface { GetPayload() *models.BasicFailedReply } // Error wraps a API error and implements both the Error and Unwrap interfaces. type Error struct { // Err is the unmodified wrapped error. Err error // merr is returned in the case that multiple errors are found as a // *models.BasicFailedReply. merr *multierror.Prefixed // err will only be populated when merr is not. err error } // Wrap creates a new Error from the passed error, it doesn't modify the // original error, but instead, tries to unwrap and extract the wrapped // errors form the go-openapi/runtime operations. The original error type // can be accessed by calling Unwrap() or errors.Unwrap/Is/As. // // If the error is of *models.BasicFailedReply type, then the errors are // unpacked into a *multierror.Prefixed which can be obtained by calling // Multierror(). // The other possible case is when the API returns an error with an unexpected // status code which is not present in the swagger spec, in which case, // the same operation will be attempted (unwrapping a BasicFailedReply) // with a fallback to unmarshal any JSON which can be read: // - error is nil, in which case nil is returned. // - error is a context.DeadlineExceeded error, which equals a timeout. // - error is of type *runtime.APIError, meaning the returned API error wasn't // defined in the Swagger spec from which the source code has been generated // - HTTP code is 449, the authenticated user needs to elevate-permissions. // - The type wraps *http.Response, the body is read and tries json.Unmarshal // to *models.BasicFailedResponse and each of the BasicFailedReplyElement // is then added to an instance of multierror.Prefixed and returned. // - The error is unknown, returns "<OperationName> (status <StatusCode)". func Wrap(err error) error { if err == nil { return nil } result := Error{Err: err} if bfr, ok := err.(basicFailedReply); ok { m := newBasicFailedReplyMultierror(errPrefix, bfr.GetPayload()) if m.ErrorOrNil() != nil { result.merr = m return &result } } if rtimeErr := unwrapRuntimeAPIError(errPrefix, err); rtimeErr != nil { var p *multierror.Prefixed if errors.As(rtimeErr, &p) { result.merr = p return &result } result.err = rtimeErr } return &result } // Unwrap returns the wrapped error. func (e *Error) Unwrap() error { if e == nil || e.Err == nil { return nil } return e.Err } // Is implements errors.Is by comparing the current value directly. func (e *Error) Is(target error) bool { return errors.Is(e.Unwrap(), target) } func (e *Error) Error() string { if e == nil || e.Err == nil { return "" } if reflect.Ptr != reflect.ValueOf(e.Err).Kind() { if e.Err == context.DeadlineExceeded { return ErrTimedOutMsg } return e.Err.Error() } if e.merr != nil && e.merr.ErrorOrNil() != nil { return e.merr.Error() } if e.err != nil { return e.err.Error() } return e.Err.Error() } // Multierror returns a multierror if there's one. func (e *Error) Multierror() *multierror.Prefixed { return e.merr } func unwrapRuntimeAPIError(prefix string, err error) error { apiErr, ok := err.(*runtime.APIError) if !ok { return nil } if apiErr.Code == httpRetryWith { return ErrMissingElevatedPermissions } if err := tryWrappedResponse(prefix, apiErr.Response); err != nil { return err } if res, _ := json.MarshalIndent(apiErr.Response, "", " "); !bytes.Equal(res, []byte("{}")) { if err := unmarshalBasicFailedReply(prefix, res); err != nil { return err } return errors.New(string(res)) } return fmt.Errorf("%s (status %d)", apiErr.OperationName, apiErr.Code) } func tryWrappedResponse(prefix string, apiError interface{}) error { v := reflect.ValueOf(apiError) if !v.IsValid() { return nil } resp := v.FieldByName("resp") if !resp.IsValid() { return nil } ptr := unsafe.Pointer(resp.Pointer()) res := (*http.Response)(ptr) b, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("failed reading error body: %w", err) } defer res.Body.Close() if err := unmarshalBasicFailedReply(prefix, b); err != nil { return err } return errors.New(string(b)) } func unmarshalBasicFailedReply(prefix string, b []byte) error { var basicFailedReply models.BasicFailedReply if err := json.Unmarshal(b, &basicFailedReply); err == nil { if err := newBasicFailedReplyMultierror(prefix, &basicFailedReply); err != nil { return err.ErrorOrNil() } } return nil }