Skip to content

Commit c4a59b4

Browse files
authored
Merge branch 'master' into serialisation
2 parents 83d340b + 9c7e5fd commit c4a59b4

23 files changed

+1694
-54
lines changed

changes/20250529101702.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Dependency upgrade: logr-1.4.3

changes/20250529160336.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `supervisor` Add option to run supervisor a fixed number of times

changes/20250529161638.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `supervisor` Add option to run a function after the supervisor stops

changes/20250529175329.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `logs` Add support for a FIFO logger that discards logs ater reading them

changes/20250530152641.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `http` Add support for HTTP client with headers

changes/20250530153502.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `http` Add utilities for headers

utils/http/header_client.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package http
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"net/url"
7+
"slices"
8+
"strings"
9+
10+
"github.com/ARM-software/golang-utils/utils/commonerrors"
11+
"github.com/ARM-software/golang-utils/utils/http/headers"
12+
)
13+
14+
type ClientWithHeaders struct {
15+
client IClient
16+
headers headers.Headers
17+
}
18+
19+
func newClientWithHeaders(underlyingClient IClient, headerValues ...string) (c *ClientWithHeaders, err error) {
20+
c = &ClientWithHeaders{
21+
headers: make(headers.Headers),
22+
}
23+
24+
if underlyingClient == nil {
25+
c.client = NewPlainHTTPClient()
26+
} else {
27+
c.client = underlyingClient
28+
}
29+
30+
for header := range slices.Chunk(headerValues, 2) {
31+
if len(header) != 2 {
32+
err = commonerrors.New(commonerrors.ErrInvalid, "headers must be supplied in key-value pairs")
33+
return
34+
}
35+
36+
c.headers.AppendHeader(header[0], header[1])
37+
}
38+
39+
return
40+
}
41+
42+
func NewHTTPClientWithHeaders(headers ...string) (clientWithHeaders IClientWithHeaders, err error) {
43+
return newClientWithHeaders(nil, headers...)
44+
}
45+
46+
func NewHTTPClientWithEmptyHeaders() (c IClientWithHeaders, err error) {
47+
return NewHTTPClientWithHeaders()
48+
}
49+
50+
func NewHTTPClientWithUnderlyingClientWithHeaders(underlyingClient IClient, headers ...string) (c IClientWithHeaders, err error) {
51+
return newClientWithHeaders(underlyingClient, headers...)
52+
}
53+
54+
func NewHTTPClientWithUnderlyingClientWithEmptyHeaders(underlyingClient IClient) (c IClientWithHeaders, err error) {
55+
return newClientWithHeaders(underlyingClient)
56+
}
57+
58+
func (c *ClientWithHeaders) do(method string, url string, body io.Reader) (*http.Response, error) {
59+
req, err := http.NewRequest(method, url, body)
60+
if err != nil {
61+
return nil, err
62+
}
63+
return c.Do(req)
64+
}
65+
66+
func (c *ClientWithHeaders) Head(url string) (*http.Response, error) {
67+
return c.do(http.MethodHead, url, nil)
68+
}
69+
70+
func (c *ClientWithHeaders) Post(url, contentType string, rawBody interface{}) (*http.Response, error) {
71+
b, err := determineBodyReader(rawBody)
72+
if err != nil {
73+
return nil, err
74+
}
75+
req, err := http.NewRequest(http.MethodPost, url, b)
76+
if err != nil {
77+
return nil, err
78+
}
79+
req.Header.Set(headers.HeaderContentType, contentType) // make sure to overrwrite any in the headers
80+
return c.client.Do(req)
81+
}
82+
83+
func (c *ClientWithHeaders) PostForm(url string, data url.Values) (*http.Response, error) {
84+
rawBody := strings.NewReader(data.Encode())
85+
return c.Post(url, headers.HeaderXWWWFormURLEncoded, rawBody)
86+
}
87+
88+
func (c *ClientWithHeaders) StandardClient() *http.Client {
89+
return c.client.StandardClient()
90+
}
91+
92+
func (c *ClientWithHeaders) Get(url string) (*http.Response, error) {
93+
return c.do(http.MethodGet, url, nil)
94+
}
95+
96+
func (c *ClientWithHeaders) Do(req *http.Request) (*http.Response, error) {
97+
c.headers.AppendToRequest(req)
98+
return c.client.Do(req)
99+
}
100+
101+
func (c *ClientWithHeaders) Delete(url string) (*http.Response, error) {
102+
return c.do(http.MethodDelete, url, nil)
103+
}
104+
105+
func (c *ClientWithHeaders) Put(url string, rawBody interface{}) (*http.Response, error) {
106+
b, err := determineBodyReader(rawBody)
107+
if err != nil {
108+
return nil, err
109+
}
110+
return c.do(http.MethodPut, url, b)
111+
}
112+
113+
func (c *ClientWithHeaders) Options(url string) (*http.Response, error) {
114+
return c.do(http.MethodOptions, url, nil)
115+
}
116+
117+
func (c *ClientWithHeaders) Close() error {
118+
c.client.StandardClient().CloseIdleConnections()
119+
return nil
120+
}
121+
122+
func (c *ClientWithHeaders) AppendHeader(key, value string) {
123+
if c.headers == nil {
124+
c.headers = make(headers.Headers)
125+
}
126+
c.headers.AppendHeader(key, value)
127+
}
128+
129+
func (c *ClientWithHeaders) RemoveHeader(key string) {
130+
if c.headers == nil {
131+
return
132+
}
133+
delete(c.headers, key)
134+
}
135+
136+
func (c *ClientWithHeaders) ClearHeaders() {
137+
c.headers = make(headers.Headers)
138+
}

utils/http/header_client_test.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package http
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"net/http"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"github.com/go-logr/logr"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
"go.uber.org/goleak"
16+
17+
"github.com/ARM-software/golang-utils/utils/http/headers"
18+
"github.com/ARM-software/golang-utils/utils/http/httptest"
19+
)
20+
21+
func TestClientWithHeadersWithDifferentBodies(t *testing.T) {
22+
clientsToTest := []struct {
23+
clientName string
24+
client func() IClient
25+
}{
26+
{
27+
clientName: "default plain client",
28+
client: NewPlainHTTPClient,
29+
},
30+
{
31+
clientName: "fast client",
32+
client: NewFastPooledClient,
33+
},
34+
{
35+
clientName: "default pooled client",
36+
client: NewDefaultPooledClient,
37+
},
38+
{
39+
clientName: "default retryable client",
40+
client: func() IClient {
41+
return NewRetryableClient()
42+
},
43+
},
44+
{
45+
clientName: "client with no retry",
46+
client: func() IClient {
47+
return NewConfigurableRetryableClient(DefaultHTTPClientConfiguration())
48+
},
49+
},
50+
{
51+
clientName: "client with basic retry",
52+
client: func() IClient {
53+
return NewConfigurableRetryableClient(DefaultRobustHTTPClientConfiguration())
54+
},
55+
},
56+
{
57+
clientName: "client with exponential backoff",
58+
client: func() IClient {
59+
return NewConfigurableRetryableClient(DefaultRobustHTTPClientConfigurationWithExponentialBackOff())
60+
},
61+
},
62+
{
63+
clientName: "client with linear backoff",
64+
client: func() IClient {
65+
return NewConfigurableRetryableClient(DefaultRobustHTTPClientConfigurationWithLinearBackOff())
66+
},
67+
},
68+
{
69+
clientName: "custom oauth client with retry after but no backoff using oauth2.Token (using custom client function with client == nil)",
70+
client: func() IClient {
71+
return NewConfigurableRetryableOauthClientWithLoggerAndCustomClient(DefaultRobustHTTPClientConfigurationWithRetryAfter(), nil, logr.Discard(), "test-token")
72+
},
73+
},
74+
{
75+
clientName: "custom oauth client with retry after but no backoff using oauth2.Token (using custom client function with client == NewPlainHTTPClient())",
76+
client: func() IClient {
77+
return NewConfigurableRetryableOauthClientWithLoggerAndCustomClient(DefaultRobustHTTPClientConfigurationWithRetryAfter(), NewPlainHTTPClient().StandardClient(), logr.Discard(), "test-token")
78+
},
79+
},
80+
{
81+
clientName: "nil",
82+
client: func() IClient {
83+
return nil
84+
},
85+
},
86+
}
87+
88+
tests := []struct {
89+
bodyType string
90+
uri string
91+
headers map[string]string
92+
body interface{}
93+
}{
94+
{
95+
bodyType: "nil",
96+
uri: "/foo/bar",
97+
headers: nil,
98+
body: nil,
99+
},
100+
{
101+
bodyType: "string",
102+
uri: "/foo/bar",
103+
headers: nil,
104+
body: "some kind of string body",
105+
},
106+
{
107+
bodyType: "string reader",
108+
uri: "/foo/bar",
109+
headers: nil,
110+
body: strings.NewReader("some kind of string body"),
111+
},
112+
{
113+
bodyType: "bytes",
114+
uri: "/foo/bar",
115+
headers: nil,
116+
body: []byte("some kind of byte body"),
117+
},
118+
{
119+
bodyType: "byte buffer",
120+
uri: "/foo/bar",
121+
headers: nil,
122+
body: bytes.NewBuffer([]byte("some kind of byte body")),
123+
},
124+
{
125+
bodyType: "byte reader",
126+
uri: "/foo/bar",
127+
headers: nil,
128+
body: bytes.NewReader([]byte("some kind of byte body")),
129+
},
130+
{
131+
bodyType: "nil + single Host",
132+
uri: "/foo/bar",
133+
headers: map[string]string{
134+
headers.HeaderHost: "example.com",
135+
},
136+
body: nil,
137+
},
138+
{
139+
bodyType: "string + WebSocket headers",
140+
uri: "/foo/bar",
141+
headers: map[string]string{
142+
headers.HeaderConnection: "Upgrade",
143+
headers.HeaderWebsocketVersion: "13",
144+
headers.HeaderWebsocketKey: "dGhlIHNhbXBsZSBub25jZQ==",
145+
headers.HeaderWebsocketProtocol: "chat, superchat",
146+
headers.HeaderWebsocketExtensions: "permessage-deflate; client_max_window_bits",
147+
},
148+
body: "hello websocket",
149+
},
150+
{
151+
bodyType: "bytes + Sunset/Deprecation",
152+
uri: "/foo/bar",
153+
headers: map[string]string{
154+
headers.HeaderSunset: "2025-12-31T23:59:59Z",
155+
headers.HeaderDeprecation: "Tue, 01 Dec 2026 00:00:00 GMT",
156+
},
157+
body: []byte("payload with deprecation headers"),
158+
},
159+
{
160+
bodyType: "byte buffer + Link",
161+
uri: "/foo/bar",
162+
headers: map[string]string{
163+
headers.HeaderLink: `<https://api.example.com/page2>; rel="next", <https://api.example.com/help>; rel="help"`,
164+
},
165+
body: bytes.NewBuffer([]byte("link header test")),
166+
},
167+
{
168+
bodyType: "reader + TUS upload headers",
169+
uri: "/foo/bar",
170+
headers: map[string]string{
171+
headers.HeaderTusVersion: "1.0.0",
172+
headers.HeaderUploadOffset: "1024",
173+
headers.HeaderUploadLength: "2048",
174+
headers.HeaderTusResumable: "1.0.0",
175+
},
176+
body: strings.NewReader("resumable upload content"),
177+
},
178+
}
179+
180+
for i := range tests {
181+
test := tests[i]
182+
for j := range clientsToTest {
183+
defer goleak.VerifyNone(t)
184+
rawClient := clientsToTest[j]
185+
var headersSlice []string
186+
for k, v := range tests[i].headers {
187+
headersSlice = append(headersSlice, k, v)
188+
}
189+
client, err := NewHTTPClientWithUnderlyingClientWithHeaders(rawClient.client(), headersSlice...)
190+
require.NoError(t, err)
191+
defer func() { _ = client.Close() }()
192+
require.NotEmpty(t, client.StandardClient())
193+
t.Run(fmt.Sprintf("local host/Client %v/Body %v", rawClient.clientName, test.bodyType), func(t *testing.T) {
194+
195+
ctx, cancel := context.WithCancel(context.Background())
196+
defer cancel()
197+
port := "28934"
198+
199+
// Mock server which always responds 201.
200+
httptest.NewTestServer(t, ctx, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
201+
w.WriteHeader(http.StatusOK)
202+
}), port)
203+
time.Sleep(100 * time.Millisecond)
204+
url := fmt.Sprintf("http://127.0.0.1:%v/%v", port, test.uri)
205+
resp, err := client.Put(url, test.body)
206+
require.NoError(t, err)
207+
_ = resp.Body.Close()
208+
bodyReader, err := determineBodyReader(test.body)
209+
require.NoError(t, err)
210+
req, err := http.NewRequest("POST", url, bodyReader)
211+
require.NoError(t, err)
212+
resp, err = client.Do(req)
213+
require.NoError(t, err)
214+
_ = resp.Body.Close()
215+
cancel()
216+
time.Sleep(100 * time.Millisecond)
217+
})
218+
clientStruct, ok := client.(*ClientWithHeaders)
219+
require.True(t, ok)
220+
221+
clientStruct.ClearHeaders()
222+
assert.Empty(t, clientStruct.headers)
223+
224+
clientStruct.AppendHeader("hello", "world")
225+
require.NotEmpty(t, clientStruct.headers)
226+
assert.Equal(t, headers.Header{Key: "hello", Value: "world"}, clientStruct.headers["hello"])
227+
228+
clientStruct.RemoveHeader("hello")
229+
assert.Empty(t, clientStruct.headers)
230+
231+
_ = client.Close()
232+
}
233+
}
234+
}

0 commit comments

Comments
 (0)