From c9e285b01cb514f6aaa78c53f5087087a08b0474 Mon Sep 17 00:00:00 2001 From: vshapovalov Date: Mon, 3 Aug 2026 14:19:32 +0300 Subject: [PATCH 1/2] fix(native): enforce ARI request timeout --- client/native/client.go | 12 +++++++++++ client/native/request_test.go | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 client/native/request_test.go diff --git a/client/native/client.go b/client/native/client.go index 63020053..f0d56283 100644 --- a/client/native/client.go +++ b/client/native/client.go @@ -49,6 +49,10 @@ type Options struct { // Logger provides a logger which should be used for this client. Logger *slog.Logger + + // RequestTimeout limits the complete lifetime of an ARI REST request. + // The package RequestTimeout value is used when this is not positive. + RequestTimeout time.Duration } // ConnectWithContext creates and connects a new Client to Asterisk ARI. @@ -143,9 +147,17 @@ func New(opts *Options) *Client { &slog.HandlerOptions{Level: slog.LevelError})) } + requestTimeout := opts.RequestTimeout + if requestTimeout <= 0 { + requestTimeout = RequestTimeout + } + return &Client{ appName: opts.Application, Options: opts, + httpClient: http.Client{ + Timeout: requestTimeout, + }, } } diff --git a/client/native/request_test.go b/client/native/request_test.go new file mode 100644 index 00000000..cd7aa511 --- /dev/null +++ b/client/native/request_test.go @@ -0,0 +1,39 @@ +package native + +import ( + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRequestStopsAtConfiguredTimeout(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) { + <-request.Context().Done() + })) + defer server.Close() + + client := New(&Options{ + URL: server.URL, + RequestTimeout: 25 * time.Millisecond, + }) + + startedAt := time.Now() + err := client.get("/channels", nil) + + require.Error(t, err) + require.Less(t, time.Since(startedAt), 500*time.Millisecond) + + var networkError net.Error + require.ErrorAs(t, err, &networkError) + require.True(t, networkError.Timeout()) +} + +func TestRequestUsesPackageDefaultTimeout(t *testing.T) { + client := New(&Options{}) + + require.Equal(t, RequestTimeout, client.httpClient.Timeout) +} From ef9a6b0b6a05263215e54a2905c4ca203a78308d Mon Sep 17 00:00:00 2001 From: vshapovalov Date: Wed, 5 Aug 2026 15:35:56 +0300 Subject: [PATCH 2/2] feat(native): support custom HTTP client --- client/native/client.go | 22 ++++++++++------------ client/native/request_test.go | 17 +++++++++++++---- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/client/native/client.go b/client/native/client.go index f0d56283..9757b9cb 100644 --- a/client/native/client.go +++ b/client/native/client.go @@ -50,9 +50,9 @@ type Options struct { // Logger provides a logger which should be used for this client. Logger *slog.Logger - // RequestTimeout limits the complete lifetime of an ARI REST request. - // The package RequestTimeout value is used when this is not positive. - RequestTimeout time.Duration + // HTTPClient sends ARI REST requests. When nil, a client using the package + // RequestTimeout is created. + HTTPClient *http.Client } // ConnectWithContext creates and connects a new Client to Asterisk ARI. @@ -147,17 +147,15 @@ func New(opts *Options) *Client { &slog.HandlerOptions{Level: slog.LevelError})) } - requestTimeout := opts.RequestTimeout - if requestTimeout <= 0 { - requestTimeout = RequestTimeout + httpClient := opts.HTTPClient + if httpClient == nil { + httpClient = &http.Client{Timeout: RequestTimeout} } return &Client{ - appName: opts.Application, - Options: opts, - httpClient: http.Client{ - Timeout: requestTimeout, - }, + appName: opts.Application, + Options: opts, + httpClient: httpClient, } } @@ -180,7 +178,7 @@ type Client struct { bus ari.Bus // httpClient is the reusable HTTP client on which commands to Asterisk are sent - httpClient http.Client + httpClient *http.Client cancel context.CancelFunc } diff --git a/client/native/request_test.go b/client/native/request_test.go index cd7aa511..a289d53c 100644 --- a/client/native/request_test.go +++ b/client/native/request_test.go @@ -10,15 +10,16 @@ import ( "github.com/stretchr/testify/require" ) -func TestRequestStopsAtConfiguredTimeout(t *testing.T) { +func TestRequestStopsAtCustomHTTPClientTimeout(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) { <-request.Context().Done() })) defer server.Close() + httpClient := &http.Client{Timeout: 25 * time.Millisecond} client := New(&Options{ - URL: server.URL, - RequestTimeout: 25 * time.Millisecond, + URL: server.URL, + HTTPClient: httpClient, }) startedAt := time.Now() @@ -32,7 +33,15 @@ func TestRequestStopsAtConfiguredTimeout(t *testing.T) { require.True(t, networkError.Timeout()) } -func TestRequestUsesPackageDefaultTimeout(t *testing.T) { +func TestNewUsesProvidedHTTPClient(t *testing.T) { + httpClient := &http.Client{} + + client := New(&Options{HTTPClient: httpClient}) + + require.Same(t, httpClient, client.httpClient) +} + +func TestNewUsesPackageDefaultRequestTimeout(t *testing.T) { client := New(&Options{}) require.Equal(t, RequestTimeout, client.httpClient.Timeout)