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
}
}
|