File: response_handlers.go

package info (click to toggle)
gitlab-agent 16.11.5-1
  • links: PTS, VCS
  • area: contrib
  • in suites: experimental
  • size: 7,072 kB
  • sloc: makefile: 193; sh: 55; ruby: 3
file content (160 lines) | stat: -rw-r--r-- 4,493 bytes parent folder | download
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,
				}
			}
		},
	}
}