...

Source file src/github.com/cli/go-gh/v2/pkg/api/errors.go

Documentation: github.com/cli/go-gh/v2/pkg/api

     1  package api
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  )
    11  
    12  // HTTPError represents an error response from the GitHub API.
    13  type HTTPError struct {
    14  	Errors     []HTTPErrorItem
    15  	Headers    http.Header
    16  	Message    string
    17  	RequestURL *url.URL
    18  	StatusCode int
    19  }
    20  
    21  // HTTPErrorItem stores additional information about an error response
    22  // returned from the GitHub API.
    23  type HTTPErrorItem struct {
    24  	Code     string
    25  	Field    string
    26  	Message  string
    27  	Resource string
    28  }
    29  
    30  // Allow HTTPError to satisfy error interface.
    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  // GraphQLError represents an error response from GitHub GraphQL API.
    41  type GraphQLError struct {
    42  	Errors []GraphQLErrorItem
    43  }
    44  
    45  // GraphQLErrorItem stores additional information about an error response
    46  // returned from the GitHub GraphQL API.
    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  // Allow GraphQLError to satisfy error interface.
    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  // Match determines if the GraphQLError is about a specific type on a specific path.
    72  // If the path argument ends with a ".", it will match all its subpaths.
    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  // HandleHTTPError parses a http.Response into a HTTPError.
   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  // Convert common error codes to human readable messages
   157  // See https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors for more details.
   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