@@ -45,7 +45,9 @@ func NewClientFromHTTP(httpClient *http.Client) *Client {
4545func AddHeader (name , value string ) ClientOption {
4646 return func (tr http.RoundTripper ) http.RoundTripper {
4747 return & funcTripper {roundTrip : func (req * http.Request ) (* http.Response , error ) {
48- req .Header .Add (name , value )
48+ if req .Header .Get (name ) == "" {
49+ req .Header .Add (name , value )
50+ }
4951 return tr .RoundTrip (req )
5052 }}
5153 }
@@ -55,11 +57,16 @@ func AddHeader(name, value string) ClientOption {
5557func AddHeaderFunc (name string , getValue func (* http.Request ) (string , error )) ClientOption {
5658 return func (tr http.RoundTripper ) http.RoundTripper {
5759 return & funcTripper {roundTrip : func (req * http.Request ) (* http.Response , error ) {
60+ if req .Header .Get (name ) != "" {
61+ return tr .RoundTrip (req )
62+ }
5863 value , err := getValue (req )
5964 if err != nil {
6065 return nil , err
6166 }
62- req .Header .Add (name , value )
67+ if value != "" {
68+ req .Header .Add (name , value )
69+ }
6370 return tr .RoundTrip (req )
6471 }}
6572 }
@@ -68,14 +75,15 @@ func AddHeaderFunc(name string, getValue func(*http.Request) (string, error)) Cl
6875// VerboseLog enables request/response logging within a RoundTripper
6976func VerboseLog (out io.Writer , logTraffic bool , colorize bool ) ClientOption {
7077 logger := & httpretty.Logger {
71- Time : true ,
72- TLS : false ,
73- Colors : colorize ,
74- RequestHeader : logTraffic ,
75- RequestBody : logTraffic ,
76- ResponseHeader : logTraffic ,
77- ResponseBody : logTraffic ,
78- Formatters : []httpretty.Formatter {& httpretty.JSONFormatter {}},
78+ Time : true ,
79+ TLS : false ,
80+ Colors : colorize ,
81+ RequestHeader : logTraffic ,
82+ RequestBody : logTraffic ,
83+ ResponseHeader : logTraffic ,
84+ ResponseBody : logTraffic ,
85+ Formatters : []httpretty.Formatter {& httpretty.JSONFormatter {}},
86+ MaxResponseBody : 10000 ,
7987 }
8088 logger .SetOutput (out )
8189 logger .SetBodyFilter (func (h http.Header ) (skip bool , err error ) {
@@ -190,7 +198,9 @@ type HTTPError struct {
190198}
191199
192200func (err HTTPError ) Error () string {
193- if err .Message != "" {
201+ if msgs := strings .SplitN (err .Message , "\n " , 2 ); len (msgs ) > 1 {
202+ return fmt .Sprintf ("HTTP %d: %s (%s)\n %s" , err .StatusCode , msgs [0 ], err .RequestURL , msgs [1 ])
203+ } else if err .Message != "" {
194204 return fmt .Sprintf ("HTTP %d: %s (%s)" , err .StatusCode , err .Message , err .RequestURL )
195205 }
196206 return fmt .Sprintf ("HTTP %d (%s)" , err .StatusCode , err .RequestURL )
@@ -222,7 +232,7 @@ func (c Client) HasMinimumScopes(hostname string) error {
222232 }()
223233
224234 if res .StatusCode != 200 {
225- return handleHTTPError (res )
235+ return HandleHTTPError (res )
226236 }
227237
228238 hasScopes := strings .Split (res .Header .Get ("X-Oauth-Scopes" ), "," )
@@ -298,7 +308,7 @@ func (c Client) REST(hostname string, method string, p string, body io.Reader, d
298308
299309 success := resp .StatusCode >= 200 && resp .StatusCode < 300
300310 if ! success {
301- return handleHTTPError (resp )
311+ return HandleHTTPError (resp )
302312 }
303313
304314 if resp .StatusCode == http .StatusNoContent {
@@ -322,7 +332,7 @@ func handleResponse(resp *http.Response, data interface{}) error {
322332 success := resp .StatusCode >= 200 && resp .StatusCode < 300
323333
324334 if ! success {
325- return handleHTTPError (resp )
335+ return HandleHTTPError (resp )
326336 }
327337
328338 body , err := ioutil .ReadAll (resp .Body )
@@ -342,13 +352,18 @@ func handleResponse(resp *http.Response, data interface{}) error {
342352 return nil
343353}
344354
345- func handleHTTPError (resp * http.Response ) error {
355+ func HandleHTTPError (resp * http.Response ) error {
346356 httpError := HTTPError {
347357 StatusCode : resp .StatusCode ,
348358 RequestURL : resp .Request .URL ,
349359 OAuthScopes : resp .Header .Get ("X-Oauth-Scopes" ),
350360 }
351361
362+ if ! jsonTypeRE .MatchString (resp .Header .Get ("Content-Type" )) {
363+ httpError .Message = resp .Status
364+ return httpError
365+ }
366+
352367 body , err := ioutil .ReadAll (resp .Body )
353368 if err != nil {
354369 httpError .Message = err .Error ()
@@ -357,14 +372,57 @@ func handleHTTPError(resp *http.Response) error {
357372
358373 var parsedBody struct {
359374 Message string `json:"message"`
375+ Errors []json.RawMessage
376+ }
377+ if err := json .Unmarshal (body , & parsedBody ); err != nil {
378+ return httpError
360379 }
361- if err := json .Unmarshal (body , & parsedBody ); err == nil {
362- httpError .Message = parsedBody .Message
380+
381+ type errorObject struct {
382+ Message string
383+ Resource string
384+ Field string
385+ Code string
386+ }
387+
388+ messages := []string {parsedBody .Message }
389+ for _ , raw := range parsedBody .Errors {
390+ switch raw [0 ] {
391+ case '"' :
392+ var errString string
393+ _ = json .Unmarshal (raw , & errString )
394+ messages = append (messages , errString )
395+ case '{' :
396+ var errInfo errorObject
397+ _ = json .Unmarshal (raw , & errInfo )
398+ msg := errInfo .Message
399+ if errInfo .Code != "custom" {
400+ msg = fmt .Sprintf ("%s.%s %s" , errInfo .Resource , errInfo .Field , errorCodeToMessage (errInfo .Code ))
401+ }
402+ if msg != "" {
403+ messages = append (messages , msg )
404+ }
405+ }
363406 }
407+ httpError .Message = strings .Join (messages , "\n " )
364408
365409 return httpError
366410}
367411
412+ func errorCodeToMessage (code string ) string {
413+ // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors
414+ switch code {
415+ case "missing" , "missing_field" :
416+ return "is missing"
417+ case "invalid" , "unprocessable" :
418+ return "is invalid"
419+ case "already_exists" :
420+ return "already exists"
421+ default :
422+ return code
423+ }
424+ }
425+
368426var jsonTypeRE = regexp .MustCompile (`[/+]json($|;)` )
369427
370428func inspectableMIMEType (t string ) bool {
0 commit comments