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 }