-
Notifications
You must be signed in to change notification settings - Fork 46
/
errors.go
171 lines (152 loc) · 4.23 KB
/
errors.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
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
)
const (
contentType = "Content-Type"
)
var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
// HTTPError represents an error response from the GitHub API.
type HTTPError struct {
Errors []HTTPErrorItem
Headers http.Header
Message string
RequestURL *url.URL
StatusCode int
}
// HTTPErrorItem stores additional information about an error response
// returned from the GitHub API.
type HTTPErrorItem struct {
Message string
Resource string
Field string
Code string
}
// Allow HTTPError to satisfy error interface.
func (err HTTPError) Error() string {
if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 {
return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1])
} else if err.Message != "" {
return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL)
}
return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL)
}
// GQLError represents an error response from GitHub GraphQL API.
type GQLError struct {
Errors []GQLErrorItem
}
// GQLErrorItem stores additional information about an error response
// returned from the GitHub GraphQL API.
type GQLErrorItem struct {
Message string
Path []interface{}
Type string
}
// Allow GQLError to satisfy error interface.
func (gr GQLError) Error() string {
errorMessages := make([]string, 0, len(gr.Errors))
for _, e := range gr.Errors {
msg := e.Message
if p := e.pathString(); p != "" {
msg = fmt.Sprintf("%s (%s)", msg, p)
}
errorMessages = append(errorMessages, msg)
}
return fmt.Sprintf("GraphQL: %s", strings.Join(errorMessages, ", "))
}
// Match determines if the GQLError is about a specific type on a specific path.
// If the path argument ends with a ".", it will match all its subpaths.
func (gr GQLError) Match(expectType, expectPath string) bool {
for _, e := range gr.Errors {
if e.Type != expectType || !matchPath(e.pathString(), expectPath) {
return false
}
}
return true
}
func (ge GQLErrorItem) pathString() string {
var res strings.Builder
for i, v := range ge.Path {
if i > 0 {
res.WriteRune('.')
}
fmt.Fprintf(&res, "%v", v)
}
return res.String()
}
func matchPath(p, expect string) bool {
if strings.HasSuffix(expect, ".") {
return strings.HasPrefix(p, expect) || p == strings.TrimSuffix(expect, ".")
}
return p == expect
}
// HandleHTTPError parses a http.Response into a HTTPError.
func HandleHTTPError(resp *http.Response) error {
httpError := HTTPError{
Headers: resp.Header,
RequestURL: resp.Request.URL,
StatusCode: resp.StatusCode,
}
if !jsonTypeRE.MatchString(resp.Header.Get(contentType)) {
httpError.Message = resp.Status
return httpError
}
body, err := io.ReadAll(resp.Body)
if err != nil {
httpError.Message = err.Error()
return httpError
}
var parsedBody struct {
Message string `json:"message"`
Errors []json.RawMessage
}
if err := json.Unmarshal(body, &parsedBody); err != nil {
return httpError
}
var messages []string
if parsedBody.Message != "" {
messages = append(messages, parsedBody.Message)
}
for _, raw := range parsedBody.Errors {
switch raw[0] {
case '"':
var errString string
_ = json.Unmarshal(raw, &errString)
messages = append(messages, errString)
httpError.Errors = append(httpError.Errors, HTTPErrorItem{Message: errString})
case '{':
var errInfo HTTPErrorItem
_ = json.Unmarshal(raw, &errInfo)
msg := errInfo.Message
if errInfo.Code != "" && errInfo.Code != "custom" {
msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code))
}
if msg != "" {
messages = append(messages, msg)
}
httpError.Errors = append(httpError.Errors, errInfo)
}
}
httpError.Message = strings.Join(messages, "\n")
return httpError
}
// Convert common error codes to human readable messages
// See https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors for more details.
func errorCodeToMessage(code string) string {
switch code {
case "missing", "missing_field":
return "is missing"
case "invalid", "unprocessable":
return "is invalid"
case "already_exists":
return "already exists"
default:
return code
}
}