github.com/hashicorp/nomad/api@v0.0.0-20240306165712-3193ac204f65/error_unexpected_response.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package api
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"slices"
    12  	"strings"
    13  	"time"
    14  )
    15  
    16  // UnexpectedResponseError tracks the components for API errors encountered when
    17  // requireOK and requireStatusIn's conditions are not met.
    18  type UnexpectedResponseError struct {
    19  	expected   []int
    20  	statusCode int
    21  	statusText string
    22  	body       string
    23  	err        error
    24  	additional error
    25  }
    26  
    27  func (e UnexpectedResponseError) HasExpectedStatuses() bool { return len(e.expected) > 0 }
    28  func (e UnexpectedResponseError) ExpectedStatuses() []int   { return e.expected }
    29  func (e UnexpectedResponseError) HasStatusCode() bool       { return e.statusCode != 0 }
    30  func (e UnexpectedResponseError) StatusCode() int           { return e.statusCode }
    31  func (e UnexpectedResponseError) HasStatusText() bool       { return e.statusText != "" }
    32  func (e UnexpectedResponseError) StatusText() string        { return e.statusText }
    33  func (e UnexpectedResponseError) HasBody() bool             { return e.body != "" }
    34  func (e UnexpectedResponseError) Body() string              { return e.body }
    35  func (e UnexpectedResponseError) HasError() bool            { return e.err != nil }
    36  func (e UnexpectedResponseError) Unwrap() error             { return e.err }
    37  func (e UnexpectedResponseError) HasAdditional() bool       { return e.additional != nil }
    38  func (e UnexpectedResponseError) Additional() error         { return e.additional }
    39  func newUnexpectedResponseError(src unexpectedResponseErrorSource, opts ...unexpectedResponseErrorOption) UnexpectedResponseError {
    40  	nErr := src()
    41  	for _, opt := range opts {
    42  		opt(nErr)
    43  	}
    44  	if nErr.statusText == "" {
    45  		// the stdlib's http.StatusText function is a good place to start
    46  		nErr.statusFromCode(http.StatusText)
    47  	}
    48  
    49  	return *nErr
    50  }
    51  
    52  // Use textual representation of the given integer code. Called when status text
    53  // is not set using the WithStatusText option.
    54  func (e UnexpectedResponseError) statusFromCode(f func(int) string) {
    55  	e.statusText = f(e.statusCode)
    56  	if !e.HasStatusText() {
    57  		e.statusText = "unknown status code"
    58  	}
    59  }
    60  
    61  func (e UnexpectedResponseError) Error() string {
    62  	var eTxt strings.Builder
    63  	eTxt.WriteString("Unexpected response code")
    64  	if e.HasBody() || e.HasStatusCode() {
    65  		eTxt.WriteString(": ")
    66  	}
    67  	if e.HasStatusCode() {
    68  		eTxt.WriteString(fmt.Sprint(e.statusCode))
    69  		if e.HasBody() {
    70  			eTxt.WriteRune(' ')
    71  		}
    72  	}
    73  	if e.HasBody() {
    74  		eTxt.WriteString(fmt.Sprintf("(%s)", e.body))
    75  	}
    76  
    77  	if e.HasAdditional() {
    78  		eTxt.WriteString(fmt.Sprintf(". Additionally, an error occurred while constructing this error (%s); the body might be truncated or missing.", e.additional.Error()))
    79  	}
    80  
    81  	return eTxt.String()
    82  }
    83  
    84  // UnexpectedResponseErrorOptions are functions passed to NewUnexpectedResponseError
    85  // to customize the created error.
    86  type unexpectedResponseErrorOption func(*UnexpectedResponseError)
    87  
    88  // withError allows the addition of a Go error that may have been encountered
    89  // while processing the response. For example, if there is an error constructing
    90  // the gzip reader to process a gzip-encoded response body.
    91  func withError(e error) unexpectedResponseErrorOption {
    92  	return func(u *UnexpectedResponseError) { u.err = e }
    93  }
    94  
    95  // withBody overwrites the Body value with the provided custom value
    96  func withBody(b string) unexpectedResponseErrorOption {
    97  	return func(u *UnexpectedResponseError) { u.body = b }
    98  }
    99  
   100  // withStatusText overwrites the StatusText value the provided custom value
   101  func withStatusText(st string) unexpectedResponseErrorOption {
   102  	return func(u *UnexpectedResponseError) { u.statusText = st }
   103  }
   104  
   105  // withExpectedStatuses provides a list of statuses that the receiving function
   106  // expected to receive. This can be used by API callers to provide more feedback
   107  // to end-users.
   108  func withExpectedStatuses(s []int) unexpectedResponseErrorOption {
   109  	return func(u *UnexpectedResponseError) { u.expected = slices.Clone(s) }
   110  }
   111  
   112  // unexpectedResponseErrorSource provides the basis for a NewUnexpectedResponseError.
   113  type unexpectedResponseErrorSource func() *UnexpectedResponseError
   114  
   115  // fromHTTPResponse read an open HTTP response, drains and closes its body as
   116  // the data for the UnexpectedResponseError.
   117  func fromHTTPResponse(resp *http.Response) unexpectedResponseErrorSource {
   118  	return func() *UnexpectedResponseError {
   119  		u := new(UnexpectedResponseError)
   120  
   121  		if resp != nil {
   122  			// collect and close the body
   123  			var buf bytes.Buffer
   124  			if _, e := io.Copy(&buf, resp.Body); e != nil {
   125  				u.additional = e
   126  			}
   127  
   128  			// Body has been tested as safe to close more than once
   129  			_ = resp.Body.Close()
   130  			body := strings.TrimSpace(buf.String())
   131  
   132  			// make and return the error
   133  			u.statusCode = resp.StatusCode
   134  			u.statusText = strings.TrimSpace(strings.TrimPrefix(resp.Status, fmt.Sprint(resp.StatusCode)))
   135  			u.body = body
   136  		}
   137  		return u
   138  	}
   139  }
   140  
   141  // fromStatusCode attempts to resolve the status code to status text using
   142  // the resolving function provided inside of the NewUnexpectedResponseError
   143  // implementation.
   144  func fromStatusCode(sc int) unexpectedResponseErrorSource {
   145  	return func() *UnexpectedResponseError { return &UnexpectedResponseError{statusCode: sc} }
   146  }
   147  
   148  // doRequestWrapper is a function that wraps the client's doRequest method
   149  // and can be used to provide error and response handling
   150  type doRequestWrapper = func(time.Duration, *http.Response, error) (time.Duration, *http.Response, error)
   151  
   152  // requireOK is used to wrap doRequest and check for a 200
   153  func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
   154  	f := requireStatusIn(http.StatusOK)
   155  	return f(d, resp, e)
   156  }
   157  
   158  // requireStatusIn is a doRequestWrapper generator that takes expected HTTP
   159  // response codes and validates that the received response code is among them
   160  func requireStatusIn(statuses ...int) doRequestWrapper {
   161  	return func(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
   162  		if e != nil {
   163  			if resp != nil {
   164  				_ = resp.Body.Close()
   165  			}
   166  			return d, nil, e
   167  		}
   168  
   169  		for _, status := range statuses {
   170  			if resp.StatusCode == status {
   171  				return d, resp, nil
   172  			}
   173  		}
   174  
   175  		return d, nil, newUnexpectedResponseError(fromHTTPResponse(resp), withExpectedStatuses(statuses))
   176  	}
   177  }