Skip to content

Commit a8d185e

Browse files
authored
fix: issue-3044 add retries to webhook (#236)
* fix: issue-3044 add retries to webhook Signed-off-by: Prema devi Kuppuswamy <premadk@gmail.com> * fix lint Signed-off-by: Prema devi Kuppuswamy <premadk@gmail.com> * fix lint Signed-off-by: Prema devi Kuppuswamy <premadk@gmail.com> * fix fmt Signed-off-by: Prema devi Kuppuswamy <premadk@gmail.com> * update go.mod Signed-off-by: Prema devi Kuppuswamy <premadk@gmail.com> * update docs Signed-off-by: Prema devi Kuppuswamy <premadk@gmail.com> --------- Signed-off-by: Prema devi Kuppuswamy <premadk@gmail.com>
1 parent 5aca046 commit a8d185e

File tree

4 files changed

+84
-15
lines changed

4 files changed

+84
-15
lines changed

docs/services/webhook.md

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ The Webhook notification service configuration includes following settings:
1111
- `headers` - optional, the headers to pass along with the webhook
1212
- `basicAuth` - optional, the basic authentication to pass along with the webhook
1313
- `insecureSkipVerify` - optional bool, true or false
14+
- `retryWaitMin` - Optional, the minimum wait time between retries. Default value: 1s.
15+
- `retryWaitMax` - Optional, the maximum wait time between retries. Default value: 5s.
16+
- `retryMax` - Optional, the maximum number of retries. Default value: 3.
17+
18+
## Retry Behavior
19+
20+
The webhook service will automatically retry the request if it fails due to network errors or if the server returns a 5xx status code. The number of retries and the wait time between retries can be configured using the `retryMax`, `retryWaitMin`, and `retryWaitMax` parameters.
21+
22+
The wait time between retries is between `retryWaitMin` and `retryWaitMax`. If all retries fail, the `Send` method will return an error.
1423

1524
## Configuration
1625

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ require (
113113
github.com/aws/aws-sdk-go-v2 v1.17.3
114114
github.com/aws/aws-sdk-go-v2/config v1.18.8
115115
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
116-
github.com/hashicorp/go-retryablehttp v0.5.3 // indirect
116+
github.com/hashicorp/go-retryablehttp v0.5.3
117117
)
118118

119119
replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.14.0

pkg/services/webhook.go

+36-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"net/http"
88
"strings"
99
texttemplate "text/template"
10+
"time"
11+
12+
"github.com/hashicorp/go-retryablehttp"
1013

1114
log "github.com/sirupsen/logrus"
1215

@@ -77,13 +80,26 @@ type BasicAuth struct {
7780
}
7881

7982
type WebhookOptions struct {
80-
URL string `json:"url"`
81-
Headers []Header `json:"headers"`
82-
BasicAuth *BasicAuth `json:"basicAuth"`
83-
InsecureSkipVerify bool `json:"insecureSkipVerify"`
83+
URL string `json:"url"`
84+
Headers []Header `json:"headers"`
85+
BasicAuth *BasicAuth `json:"basicAuth"`
86+
InsecureSkipVerify bool `json:"insecureSkipVerify"`
87+
RetryWaitMin time.Duration `json:"retryWaitMin"`
88+
RetryWaitMax time.Duration `json:"retryWaitMax"`
89+
RetryMax int `json:"retryMax"`
8490
}
8591

8692
func NewWebhookService(opts WebhookOptions) NotificationService {
93+
// Set default values if fields are zero
94+
if opts.RetryWaitMin == 0 {
95+
opts.RetryWaitMin = 1 * time.Second
96+
}
97+
if opts.RetryWaitMax == 0 {
98+
opts.RetryWaitMax = 5 * time.Second
99+
}
100+
if opts.RetryMax == 0 {
101+
opts.RetryMax = 3
102+
}
87103
return &webhookService{opts: opts}
88104
}
89105

@@ -133,31 +149,37 @@ func (r *request) applyOverridesFrom(notification WebhookNotification) {
133149
}
134150
}
135151

136-
func (r *request) intoHttpRequest(service *webhookService) (*http.Request, error) {
137-
req, err := http.NewRequest(r.method, r.url, bytes.NewBufferString(r.body))
152+
func (r *request) intoRetryableHttpRequest(service *webhookService) (*retryablehttp.Request, error) {
153+
retryReq, err := retryablehttp.NewRequest(r.method, r.url, bytes.NewBufferString(r.body))
138154
if err != nil {
139155
return nil, err
140156
}
141157
for _, header := range service.opts.Headers {
142-
req.Header.Set(header.Name, header.Value)
158+
retryReq.Header.Set(header.Name, header.Value)
143159
}
144160
if service.opts.BasicAuth != nil {
145-
req.SetBasicAuth(service.opts.BasicAuth.Username, service.opts.BasicAuth.Password)
161+
retryReq.SetBasicAuth(service.opts.BasicAuth.Username, service.opts.BasicAuth.Password)
146162
}
147-
return req, nil
163+
return retryReq, nil
148164
}
149165

150166
func (r *request) execute(service *webhookService) (*http.Response, error) {
151-
req, err := r.intoHttpRequest(service)
167+
req, err := r.intoRetryableHttpRequest(service)
152168
if err != nil {
153169
return nil, err
154170
}
155171

156-
client := http.Client{
157-
Transport: httputil.NewLoggingRoundTripper(
158-
httputil.NewTransport(r.url, service.opts.InsecureSkipVerify),
159-
log.WithField("service", r.destService)),
172+
transport := httputil.NewLoggingRoundTripper(
173+
httputil.NewTransport(r.url, service.opts.InsecureSkipVerify),
174+
log.WithField("service", r.destService))
175+
176+
client := retryablehttp.NewClient()
177+
client.HTTPClient = &http.Client{
178+
Transport: transport,
160179
}
180+
client.RetryWaitMin = service.opts.RetryWaitMin
181+
client.RetryWaitMax = service.opts.RetryWaitMax
182+
client.RetryMax = service.opts.RetryMax
161183

162184
return client.Do(req)
163185
}

pkg/services/webhook_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"net/http"
77
"net/http/httptest"
8+
"strings"
89
"testing"
910
"text/template"
1011

@@ -123,3 +124,40 @@ func TestGetTemplater_Webhook(t *testing.T) {
123124
assert.Equal(t, notification.Webhook["github"].Body, "hello")
124125
assert.Equal(t, notification.Webhook["github"].Path, "world")
125126
}
127+
128+
func TestWebhookService_Send_Retry(t *testing.T) {
129+
// Set up a mock server to receive requests
130+
count := 0
131+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132+
count++
133+
if count < 5 {
134+
w.WriteHeader(http.StatusInternalServerError)
135+
} else {
136+
w.WriteHeader(http.StatusOK)
137+
}
138+
}))
139+
defer server.Close()
140+
141+
service := NewWebhookService(WebhookOptions{
142+
BasicAuth: &BasicAuth{Username: "testUsername", Password: "testPassword"},
143+
URL: server.URL,
144+
Headers: []Header{{Name: "testHeader", Value: "testHeaderValue"}},
145+
InsecureSkipVerify: true,
146+
})
147+
err := service.Send(
148+
Notification{
149+
Webhook: map[string]WebhookNotification{
150+
"test": {Body: "hello world", Method: http.MethodPost},
151+
},
152+
}, Destination{Recipient: "test", Service: "test"})
153+
154+
// Check if the error is due to a server error after retries
155+
if !strings.Contains(err.Error(), "giving up after 4 attempts") {
156+
t.Errorf("Expected giving up after 4 attempts, got %v", err)
157+
}
158+
159+
// Check that the mock server received 4 requests
160+
if count != 4 {
161+
t.Errorf("Expected 4 requests, got %d", count)
162+
}
163+
}

0 commit comments

Comments
 (0)