-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathrequest.go
205 lines (178 loc) · 5.99 KB
/
request.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package minercraft
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
// Retryable can be implemented to identify a struct as retryable, in this case an error can be deemed retryable.
type Retryable interface {
// IsRetryable if a method has this method then it's a retryable error.
IsRetryable()
}
// IsRetryable can be passed an error to check if it is retryable
func IsRetryable(err error) bool {
var e Retryable
return errors.As(err, &e)
}
// ErrRetryable indicates a retryable error.
//
// To check an error is a retryable error do:
//
// errors.Is(err, minercraft.Retryable)
type ErrRetryable struct{ err error }
func (e ErrRetryable) Error() string {
return e.err.Error()
}
// IsRetryable returns true denoting this is retryable.
func (e ErrRetryable) IsRetryable() {}
// Is allows the underlying error to be checked that it is a certain error type.
func (e ErrRetryable) Is(err error) bool { return errors.Is(e.err, err) }
// As will return true if the error can be cast to the target.
func (e ErrRetryable) As(target interface{}) bool { return errors.As(e.err, target) }
// ErrorResponse is the response returned from mAPI on error.
type ErrorResponse struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
TraceID string `json:"traceId"`
// Errors will return a list of formatting errors in the case of a bad request
// being sent to mAPI.
Errors map[string][]string `json:"errors"`
}
// Error defines the ErrorResponse as an error, an error can be converted
// to it using the below:
//
// var errResp ErrorResponse
// if errors.As(testErr, &errResp) {
// // handle error
// fmt.Println(errResp.Title)
// }
func (e ErrorResponse) Error() string {
sb := strings.Builder{}
for field, warnings := range e.Errors {
sb.WriteString("[" + field + ": ")
sb.WriteString(strings.Join(warnings, ", "))
sb.WriteString("]")
}
return fmt.Sprintf("title: %s \n detail: %s \n traceID: %s \n validation errors: %s",
e.Title, e.Detail, e.TraceID, sb.String())
}
// RequestResponse is the response from a request
type RequestResponse struct {
BodyContents []byte `json:"body_contents"` // Raw body response
Error error `json:"error"` // If an error occurs
Method string `json:"method"` // Method is the HTTP method used
PostData string `json:"post_data"` // PostData is the post data submitted if POST/PUT request
StatusCode int `json:"status_code"` // StatusCode is the last code from the request
URL string `json:"url"` // URL is used for the request
}
// httpPayload is used for a httpRequest
type httpPayload struct {
Method string `json:"method"`
URL string `json:"url"`
Token string `json:"token"`
Data []byte `json:"data"`
Headers map[string]string `json:"headers"`
}
// httpRequest is a generic request wrapper that can be used without constraints.
//
// If response.Error isn't nil it can be checked for being retryable by calling errors.Is(err, minercraft.Retryable),
// this means the request returned an intermittent / transient error and can be retried depending on client
// requirements.
//
// It can also be converted to the ErrorResponse type to get the error detail as shown:
//
// var errResp ErrorResponse
// if errors.As(testErr, &errResp) {
// // handle error
// fmt.Println(errResp.Title)
// }
func httpRequest(ctx context.Context, client *Client,
payload *httpPayload) (response *RequestResponse) {
// Set reader
var bodyReader io.Reader
// Start the response
response = new(RequestResponse)
// Add post data if applicable
if payload.Method == http.MethodPost || payload.Method == http.MethodPut {
bodyReader = bytes.NewBuffer(payload.Data)
response.PostData = string(payload.Data)
}
// Store for debugging purposes
response.Method = payload.Method
response.URL = payload.URL
// Start the request
var request *http.Request
if request, response.Error = http.NewRequestWithContext(
ctx, payload.Method, payload.URL, bodyReader,
); response.Error != nil {
return
}
for key, value := range payload.Headers {
request.Header.Set(key, value)
}
// Change the header (user agent is in case they block default Go user agents)
request.Header.Set("User-Agent", client.Options.UserAgent)
// Set the content type on Method
if payload.Method == http.MethodPost || payload.Method == http.MethodPut {
request.Header.Set("Content-Type", "application/json")
}
// Set a token if supplied
if len(payload.Token) > 0 {
request.Header.Set("Authorization", payload.Token)
}
// Fire the http request
var resp *http.Response
if resp, response.Error = client.httpClient.Do(request); response.Error != nil {
if resp != nil {
response.StatusCode = resp.StatusCode
}
return
}
// Close the response body
defer func() {
_ = resp.Body.Close()
}()
// Set the status
response.StatusCode = resp.StatusCode
if resp.Body != nil {
// Read the body
response.BodyContents, response.Error = io.ReadAll(resp.Body)
}
// Check status code
if http.StatusOK == resp.StatusCode {
return
}
// indicates that resubmitting this request could be successful when mAPI
// is available again.
retryable := response.StatusCode >= 500 && response.StatusCode <= 599
// unexpected status, write an error.
if response.BodyContents == nil {
// There's no "body" present, so just echo status code.
statusErr := fmt.Errorf("status code: %d does not match %d", resp.StatusCode, http.StatusOK)
if !retryable {
response.Error = statusErr
return
}
response.Error = ErrRetryable{err: statusErr}
return
}
// Have a "body" so map to an error type and add to the error message.
var errBody ErrorResponse
if err := json.Unmarshal(response.BodyContents, &errBody); err != nil {
response.Error = fmt.Errorf("failed to unmarshal mapi error response: %w", err)
return
}
if retryable {
response.Error = ErrRetryable{err: errBody}
return
}
response.Error = errBody
return
}