errors.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package client
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io/ioutil"
  7. "mime"
  8. "net/http"
  9. "github.com/docker/distribution/registry/api/errcode"
  10. "github.com/docker/distribution/registry/client/auth/challenge"
  11. )
  12. // ErrNoErrorsInBody is returned when an HTTP response body parses to an empty
  13. // errcode.Errors slice.
  14. var ErrNoErrorsInBody = errors.New("no error details found in HTTP response body")
  15. // UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
  16. // returned when making a registry api call.
  17. type UnexpectedHTTPStatusError struct {
  18. Status string
  19. }
  20. func (e *UnexpectedHTTPStatusError) Error() string {
  21. return fmt.Sprintf("received unexpected HTTP status: %s", e.Status)
  22. }
  23. // UnexpectedHTTPResponseError is returned when an expected HTTP status code
  24. // is returned, but the content was unexpected and failed to be parsed.
  25. type UnexpectedHTTPResponseError struct {
  26. ParseErr error
  27. StatusCode int
  28. Response []byte
  29. }
  30. func (e *UnexpectedHTTPResponseError) Error() string {
  31. return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response))
  32. }
  33. func parseHTTPErrorResponse(resp *http.Response) error {
  34. var errors errcode.Errors
  35. body, err := ioutil.ReadAll(resp.Body)
  36. if err != nil {
  37. return err
  38. }
  39. statusCode := resp.StatusCode
  40. ctHeader := resp.Header.Get("Content-Type")
  41. if ctHeader == "" {
  42. return makeError(statusCode, string(body))
  43. }
  44. contentType, _, err := mime.ParseMediaType(ctHeader)
  45. if err != nil {
  46. return fmt.Errorf("failed parsing content-type: %w", err)
  47. }
  48. if contentType != "application/json" && contentType != "application/vnd.api+json" {
  49. return makeError(statusCode, string(body))
  50. }
  51. // For backward compatibility, handle irregularly formatted
  52. // messages that contain a "details" field.
  53. var detailsErr struct {
  54. Details string `json:"details"`
  55. }
  56. err = json.Unmarshal(body, &detailsErr)
  57. if err == nil && detailsErr.Details != "" {
  58. return makeError(statusCode, detailsErr.Details)
  59. }
  60. if err := json.Unmarshal(body, &errors); err != nil {
  61. return &UnexpectedHTTPResponseError{
  62. ParseErr: err,
  63. StatusCode: statusCode,
  64. Response: body,
  65. }
  66. }
  67. if len(errors) == 0 {
  68. // If there was no error specified in the body, return
  69. // UnexpectedHTTPResponseError.
  70. return &UnexpectedHTTPResponseError{
  71. ParseErr: ErrNoErrorsInBody,
  72. StatusCode: statusCode,
  73. Response: body,
  74. }
  75. }
  76. return errors
  77. }
  78. func makeError(statusCode int, details string) error {
  79. switch statusCode {
  80. case http.StatusUnauthorized:
  81. return errcode.ErrorCodeUnauthorized.WithMessage(details)
  82. case http.StatusForbidden:
  83. return errcode.ErrorCodeDenied.WithMessage(details)
  84. case http.StatusTooManyRequests:
  85. return errcode.ErrorCodeTooManyRequests.WithMessage(details)
  86. default:
  87. return errcode.ErrorCodeUnknown.WithMessage(details)
  88. }
  89. }
  90. func makeErrorList(err error) []error {
  91. if errL, ok := err.(errcode.Errors); ok {
  92. return []error(errL)
  93. }
  94. return []error{err}
  95. }
  96. func mergeErrors(err1, err2 error) error {
  97. return errcode.Errors(append(makeErrorList(err1), makeErrorList(err2)...))
  98. }
  99. // HandleErrorResponse returns error parsed from HTTP response for an
  100. // unsuccessful HTTP response code (in the range 400 - 499 inclusive). An
  101. // UnexpectedHTTPStatusError returned for response code outside of expected
  102. // range.
  103. func HandleErrorResponse(resp *http.Response) error {
  104. if resp.StatusCode >= 400 && resp.StatusCode < 500 {
  105. // Check for OAuth errors within the `WWW-Authenticate` header first
  106. // See https://tools.ietf.org/html/rfc6750#section-3
  107. for _, c := range challenge.ResponseChallenges(resp) {
  108. if c.Scheme == "bearer" {
  109. var err errcode.Error
  110. // codes defined at https://tools.ietf.org/html/rfc6750#section-3.1
  111. switch c.Parameters["error"] {
  112. case "invalid_token":
  113. err.Code = errcode.ErrorCodeUnauthorized
  114. case "insufficient_scope":
  115. err.Code = errcode.ErrorCodeDenied
  116. default:
  117. continue
  118. }
  119. if description := c.Parameters["error_description"]; description != "" {
  120. err.Message = description
  121. } else {
  122. err.Message = err.Code.Message()
  123. }
  124. return mergeErrors(err, parseHTTPErrorResponse(resp))
  125. }
  126. }
  127. err := parseHTTPErrorResponse(resp)
  128. if uErr, ok := err.(*UnexpectedHTTPResponseError); ok && resp.StatusCode == 401 {
  129. return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response)
  130. }
  131. return err
  132. }
  133. return &UnexpectedHTTPStatusError{Status: resp.Status}
  134. }
  135. // SuccessStatus returns true if the argument is a successful HTTP response
  136. // code (in the range 200 - 399 inclusive).
  137. func SuccessStatus(status int) bool {
  138. return status >= 200 && status <= 399
  139. }