1 package api
2
3 import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "strings"
10 )
11
12
13 type HTTPError struct {
14 Errors []HTTPErrorItem
15 Headers http.Header
16 Message string
17 RequestURL *url.URL
18 StatusCode int
19 }
20
21
22
23 type HTTPErrorItem struct {
24 Code string
25 Field string
26 Message string
27 Resource string
28 }
29
30
31 func (err *HTTPError) Error() string {
32 if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 {
33 return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1])
34 } else if err.Message != "" {
35 return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL)
36 }
37 return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL)
38 }
39
40
41 type GraphQLError struct {
42 Errors []GraphQLErrorItem
43 }
44
45
46
47 type GraphQLErrorItem struct {
48 Message string
49 Locations []struct {
50 Line int
51 Column int
52 }
53 Path []interface{}
54 Extensions map[string]interface{}
55 Type string
56 }
57
58
59 func (gr *GraphQLError) Error() string {
60 errorMessages := make([]string, 0, len(gr.Errors))
61 for _, e := range gr.Errors {
62 msg := e.Message
63 if p := e.pathString(); p != "" {
64 msg = fmt.Sprintf("%s (%s)", msg, p)
65 }
66 errorMessages = append(errorMessages, msg)
67 }
68 return fmt.Sprintf("GraphQL: %s", strings.Join(errorMessages, ", "))
69 }
70
71
72
73 func (gr *GraphQLError) Match(expectType, expectPath string) bool {
74 for _, e := range gr.Errors {
75 if e.Type != expectType || !matchPath(e.pathString(), expectPath) {
76 return false
77 }
78 }
79 return true
80 }
81
82 func (ge GraphQLErrorItem) pathString() string {
83 var res strings.Builder
84 for i, v := range ge.Path {
85 if i > 0 {
86 res.WriteRune('.')
87 }
88 fmt.Fprintf(&res, "%v", v)
89 }
90 return res.String()
91 }
92
93 func matchPath(p, expect string) bool {
94 if strings.HasSuffix(expect, ".") {
95 return strings.HasPrefix(p, expect) || p == strings.TrimSuffix(expect, ".")
96 }
97 return p == expect
98 }
99
100
101 func HandleHTTPError(resp *http.Response) error {
102 httpError := &HTTPError{
103 Headers: resp.Header,
104 RequestURL: resp.Request.URL,
105 StatusCode: resp.StatusCode,
106 }
107
108 if !jsonTypeRE.MatchString(resp.Header.Get(contentType)) {
109 httpError.Message = resp.Status
110 return httpError
111 }
112
113 body, err := io.ReadAll(resp.Body)
114 if err != nil {
115 httpError.Message = err.Error()
116 return httpError
117 }
118
119 var parsedBody struct {
120 Message string `json:"message"`
121 Errors []json.RawMessage
122 }
123 if err := json.Unmarshal(body, &parsedBody); err != nil {
124 return httpError
125 }
126
127 var messages []string
128 if parsedBody.Message != "" {
129 messages = append(messages, parsedBody.Message)
130 }
131 for _, raw := range parsedBody.Errors {
132 switch raw[0] {
133 case '"':
134 var errString string
135 _ = json.Unmarshal(raw, &errString)
136 messages = append(messages, errString)
137 httpError.Errors = append(httpError.Errors, HTTPErrorItem{Message: errString})
138 case '{':
139 var errInfo HTTPErrorItem
140 _ = json.Unmarshal(raw, &errInfo)
141 msg := errInfo.Message
142 if errInfo.Code != "" && errInfo.Code != "custom" {
143 msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code))
144 }
145 if msg != "" {
146 messages = append(messages, msg)
147 }
148 httpError.Errors = append(httpError.Errors, errInfo)
149 }
150 }
151 httpError.Message = strings.Join(messages, "\n")
152
153 return httpError
154 }
155
156
157
158 func errorCodeToMessage(code string) string {
159 switch code {
160 case "missing", "missing_field":
161 return "is missing"
162 case "invalid", "unprocessable":
163 return "is invalid"
164 case "already_exists":
165 return "already exists"
166 default:
167 return code
168 }
169 }
170
View as plain text