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
|
package gitlab
import (
"fmt"
"io"
"net/http"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v16/internal/tool/errz"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v16/internal/tool/httpz"
"google.golang.org/protobuf/encoding/protojson"
)
type ResponseHandlerStruct struct {
AcceptHeader string
HandleFunc func(*http.Response, error) error
}
type ErrHandler func(resp *http.Response) error
func (r ResponseHandlerStruct) Handle(resp *http.Response, err error) error {
return r.HandleFunc(resp, err)
}
func (r ResponseHandlerStruct) Accept() string {
return r.AcceptHeader
}
func NakedResponseHandler(response **http.Response) ResponseHandler {
return ResponseHandlerStruct{
HandleFunc: func(r *http.Response, err error) error {
if err != nil {
return err
}
*response = r
return nil
},
}
}
func ProtoJSONResponseHandler(response ValidatableMessage) ResponseHandler {
return ProtoJSONResponseHandlerWithErr(response, func(resp *http.Response) error {
return defaultErrorHandler(resp)
})
}
func ProtoJSONResponseHandlerWithStructuredErrReason(response ValidatableMessage) ResponseHandler {
return ProtoJSONResponseHandlerWithErr(response, defaultErrorHandlerWithReason)
}
func ProtoJSONResponseHandlerWithErr(response ValidatableMessage, errHandler ErrHandler) ResponseHandler {
return ResponseHandlerStruct{
AcceptHeader: "application/json",
HandleFunc: handleOkResponse(func(body []byte) error { //nolint:bodyclose
err := protojson.UnmarshalOptions{
DiscardUnknown: true,
}.Unmarshal(body, response)
if err != nil {
return fmt.Errorf("protojson.Unmarshal: %w", err)
}
if err = response.ValidateAll(); err != nil {
return fmt.Errorf("ValidateAll: %w", err)
}
return nil
}, errHandler),
}
}
func defaultErrorHandler(resp *http.Response) *ClientError {
path := ""
if resp.Request != nil && resp.Request.URL != nil {
path = resp.Request.URL.Path
}
return &ClientError{
StatusCode: int32(resp.StatusCode),
Path: path,
}
}
// defaultErrorHandlerWithReason tries to add an error reason from the response body.
// If no reason can be found, none is added to the response
func defaultErrorHandlerWithReason(resp *http.Response) error {
e := defaultErrorHandler(resp)
contentTypes := resp.Header[httpz.ContentTypeHeader]
if len(contentTypes) == 0 {
e.Reason = "<unknown reason: missing content type header to read reason>"
return e
}
contentType := contentTypes[0]
if !httpz.IsContentType(contentType, "application/json") {
e.Reason = fmt.Sprintf("<unknown reason: expected application/json content type, but got %s>", contentType)
return e
}
body, err := io.ReadAll(resp.Body)
if err != nil {
e.Reason = fmt.Sprintf("<unknown reason: unable to read response body: %s>", err)
return e
}
var message DefaultApiError
err = protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(body, &message)
if err != nil {
e.Reason = fmt.Sprintf("<unknown reason: %s>", err)
return e
}
e.Reason = message.Message
return e
}
func handleOkResponse(h func(body []byte) error, errHandler ErrHandler) func(*http.Response, error) error {
return func(resp *http.Response, err error) (retErr error) {
if err != nil {
return err
}
defer errz.DiscardAndClose(resp.Body, &retErr)
switch resp.StatusCode {
case http.StatusOK, http.StatusCreated:
contentType := resp.Header.Get(httpz.ContentTypeHeader)
if !httpz.IsContentType(contentType, "application/json") {
return fmt.Errorf("unexpected %s in response: %q", httpz.ContentTypeHeader, contentType)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("response body read: %w", err)
}
return h(body)
default: // Unexpected status
return errHandler(resp)
}
}
}
// NoContentResponseHandler can be used when no response is expected or response must be discarded.
func NoContentResponseHandler() ResponseHandler {
return ResponseHandlerStruct{
HandleFunc: func(resp *http.Response, err error) (retErr error) {
if err != nil {
return err
}
defer errz.DiscardAndClose(resp.Body, &retErr)
switch resp.StatusCode {
case http.StatusOK, http.StatusNoContent:
return nil
default: // Unexpected status
path := ""
if resp.Request != nil && resp.Request.URL != nil {
path = resp.Request.URL.Path
}
return &ClientError{
StatusCode: int32(resp.StatusCode),
Path: path,
}
}
},
}
}
|