@@ -51,16 +51,18 @@ const (
5151)
5252
5353type webhookClient struct {
54- URL string
55- Secret * string
56- httpClient * http.Client
54+ URL string
55+ Secret * string
56+ httpClient * http.Client
57+ retryDelays []time.Duration
5758}
5859
5960func NewWebhookService (url string , secret * string ) * webhookClient {
6061 return & webhookClient {
61- URL : url ,
62- Secret : secret ,
63- httpClient : & http.Client {Transport : utils .EgressTransport },
62+ URL : url ,
63+ Secret : secret ,
64+ httpClient : & http.Client {Transport : utils .EgressTransport },
65+ retryDelays : []time.Duration {1 * time .Second , 5 * time .Second , 10 * time .Second },
6466 }
6567}
6668
@@ -73,198 +75,123 @@ func (c *webhookClient) CreateRequest(ctx context.Context, method, url string, b
7375 ctx , cancel := context .WithTimeout (ctx , 120 * time .Second )
7476 defer cancel ()
7577
76- // Retry logic with delays: 1s, 5s, 10s
77- retryDelays := []time.Duration {1 * time .Second , 5 * time .Second , 10 * time .Second }
78-
79- var resp * http.Response
78+ var (
79+ resp * http.Response
80+ lastErr error
81+ )
82+
83+ for i , delay := range c .retryDelays {
84+ // Drain and close the previous iteration's body so the connection can be reused.
85+ if resp != nil {
86+ _ , _ = io .Copy (io .Discard , resp .Body )
87+ resp .Body .Close ()
88+ resp = nil
89+ }
8090
81- for i , delay := range retryDelays {
8291 req , err := http .NewRequestWithContext (ctx , method , url , bytes .NewReader (bodyBytes ))
8392 if err != nil {
8493 return nil , err
8594 }
86-
8795 if c .Secret != nil {
8896 req .Header .Set ("X-Webhook-Secret" , * c .Secret )
8997 }
90-
9198 req .Header .Set ("Content-Type" , "application/json" )
9299
93- resp , err = c .httpClient .Do (req )
100+ resp , lastErr = c .httpClient .Do (req )
94101
95- if err == nil && resp != nil && resp .StatusCode >= 200 && resp .StatusCode < 300 {
102+ // Don't retry on 2xx or permanent 4xx — only 408/429 are retryable in the 4xx range.
103+ if lastErr == nil && resp .StatusCode < 500 &&
104+ resp .StatusCode != http .StatusRequestTimeout &&
105+ resp .StatusCode != http .StatusTooManyRequests {
96106 return resp , nil
97107 }
98108
99- if i == len (retryDelays )- 1 {
100- return nil , fmt . Errorf ( "webhook request failed with no response" )
109+ if i == len (c . retryDelays )- 1 {
110+ break
101111 }
102112
103- time .Sleep (delay )
113+ select {
114+ case <- ctx .Done ():
115+ if resp != nil {
116+ _ , _ = io .Copy (io .Discard , resp .Body )
117+ resp .Body .Close ()
118+ }
119+ return nil , ctx .Err ()
120+ case <- time .After (delay ):
121+ }
104122 }
105123
106- // This should never be reached due to the break condition above
107- return nil , fmt .Errorf ("unexpected end of retry loop" )
108-
124+ if lastErr != nil {
125+ // http.Client.Do can return a non-nil response together with an error
126+ // (e.g. CheckRedirect failures). Drain and close so the connection isn't leaked.
127+ if resp != nil {
128+ _ , _ = io .Copy (io .Discard , resp .Body )
129+ resp .Body .Close ()
130+ }
131+ return nil , lastErr
132+ }
133+ return resp , nil
109134}
110135
111- func (c * webhookClient ) SendSBOM (ctx context.Context , SBOM cdx.BOM , org shared.OrgObject , project shared.ProjectObject , asset shared.AssetObject , assetVersion shared.AssetVersionObject , artifact shared.ArtifactObject ) error {
112-
136+ func (c * webhookClient ) send (ctx context.Context , webhookType WebhookType , payload any , org shared.OrgObject , project shared.ProjectObject , asset shared.AssetObject , assetVersion shared.AssetVersionObject , artifact shared.ArtifactObject ) error {
113137 body := WebhookStruct {
114138 Organization : org ,
115139 Project : project ,
116140 Asset : asset ,
117141 AssetVersion : assetVersion ,
118- Payload : SBOM ,
119- Type : WebhookTypeSBOM ,
142+ Payload : payload ,
143+ Type : webhookType ,
120144 Artifact : artifact ,
121145 }
122146
123147 var buf bytes.Buffer
124- err := json .NewEncoder (& buf ).Encode (body )
125- if err != nil {
148+ if err := json .NewEncoder (& buf ).Encode (body ); err != nil {
126149 return err
127150 }
128151
129152 resp , err := c .CreateRequest (ctx , "POST" , c .URL , & buf )
130153 if err != nil {
131154 return err
132155 }
133- if resp == nil {
134- return fmt .Errorf ("received nil response when sending SBOM" )
135- }
136156 defer resp .Body .Close ()
137- if resp .StatusCode != http .StatusOK && resp .StatusCode != http .StatusCreated {
138- return fmt .Errorf ("failed to send SBOM, status: %s" , resp .Status )
139- }
140157
158+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
159+ return fmt .Errorf ("webhook %s failed, status: %s" , webhookType , resp .Status )
160+ }
141161 return nil
142162}
143163
144- func (c * webhookClient ) SendFirstPartyVulnerabilities (ctx context.Context , vuln []dtos.FirstPartyVulnDTO , org shared.OrgObject , project shared.ProjectObject , asset shared.AssetObject , assetVersion shared.AssetVersionObject ) error {
145- return nil
146-
147- /*body := WebhookStruct{
148- Organization: org,
149- Project: project,
150- Asset: asset,
151- AssetVersion: assetVersion,
152- Payload: vuln,
153- Type: WebhookTypeFirstPartyVulnerabilities,
154- }
155-
156- var buf bytes.Buffer
157- err := json.NewEncoder(&buf).Encode(body)
158- if err != nil {
159- return err
160- }
161-
162- resp, err := c.CreateRequest("POST", c.URL, &buf)
163- if err != nil {
164- return err
165- }
166- defer resp.Body.Close()
167- if resp.StatusCode != http.StatusOK {
168- return fmt.Errorf("failed to send vulnerability, status: %s,", resp.Status)
169- }
164+ func (c * webhookClient ) SendSBOM (ctx context.Context , SBOM cdx.BOM , org shared.OrgObject , project shared.ProjectObject , asset shared.AssetObject , assetVersion shared.AssetVersionObject , artifact shared.ArtifactObject ) error {
165+ return c .send (ctx , WebhookTypeSBOM , SBOM , org , project , asset , assetVersion , artifact )
166+ }
170167
171- return nil*/
168+ func (c * webhookClient ) SendFirstPartyVulnerabilities (ctx context.Context , vuln []dtos.FirstPartyVulnDTO , org shared.OrgObject , project shared.ProjectObject , asset shared.AssetObject , assetVersion shared.AssetVersionObject ) error {
169+ return c .send (ctx , WebhookTypeFirstPartyVulnerabilities , vuln , org , project , asset , assetVersion , shared.ArtifactObject {})
172170}
173171
174172func (c * webhookClient ) SendDependencyVulnerabilities (ctx context.Context , vuln []dtos.DependencyVulnDTO , org shared.OrgObject , project shared.ProjectObject , asset shared.AssetObject , assetVersion shared.AssetVersionObject , artifact shared.ArtifactObject ) error {
175-
176- body := WebhookStruct {
177- Organization : org ,
178- Project : project ,
179- Asset : asset ,
180- AssetVersion : assetVersion ,
181- Payload : vuln ,
182- Artifact : artifact ,
183- Type : WebhookTypeDependencyVulnerabilities ,
184- }
185-
186- var buf bytes.Buffer
187- err := json .NewEncoder (& buf ).Encode (body )
188- if err != nil {
189- return err
190- }
191-
192- resp , err := c .CreateRequest (ctx , "POST" , c .URL , & buf )
193- if err != nil {
194- return err
195- }
196- if resp == nil {
197- return fmt .Errorf ("received nil response when sending dependency vulnerabilities" )
198- }
199- defer resp .Body .Close ()
200- if resp .StatusCode != http .StatusOK {
201- return fmt .Errorf ("failed to send vulnerability, status: %s" , resp .Status )
202- }
203-
204- return nil
173+ return c .send (ctx , WebhookTypeDependencyVulnerabilities , vuln , org , project , asset , assetVersion , artifact )
205174}
206175
207176func (c * webhookClient ) SendTest (ctx context.Context , org shared.OrgObject , project shared.ProjectObject , asset shared.AssetObject , assetVersion shared.AssetVersionObject , payloadType TestPayloadType ) error {
177+ payload , webhookType := testPayload (payloadType )
178+ return c .send (ctx , webhookType , payload , org , project , asset , assetVersion , shared.ArtifactObject {})
179+ }
208180
209- var payload any
210- var webhookType WebhookType
211-
181+ func testPayload (payloadType TestPayloadType ) (any , WebhookType ) {
212182 switch payloadType {
213- case TestPayloadTypeEmpty :
214- payload = map [string ]any {
215- "message" : "This is a test webhook from DevGuard" ,
216- "timestamp" : time .Now ().UTC ().Format (time .RFC3339 ),
217- }
218- webhookType = WebhookTypeTest
219-
220183 case TestPayloadTypeSampleSBOM :
221- payload = createSampleSBOM ()
222- webhookType = WebhookTypeSBOM
223-
184+ return createSampleSBOM (), WebhookTypeSBOM
224185 case TestPayloadTypeSampleDependencyVulns :
225- payload = createSampleDependencyVulns ()
226- webhookType = WebhookTypeDependencyVulnerabilities
227-
186+ return createSampleDependencyVulns (), WebhookTypeDependencyVulnerabilities
228187 case TestPayloadTypeSampleFirstPartyVulns :
229- payload = createSampleFirstPartyVulns ()
230- webhookType = WebhookTypeFirstPartyVulnerabilities
231-
188+ return createSampleFirstPartyVulns (), WebhookTypeFirstPartyVulnerabilities
232189 default :
233- payload = map [string ]any {
190+ return map [string ]any {
234191 "message" : "This is a test webhook from DevGuard" ,
235192 "timestamp" : time .Now ().UTC ().Format (time .RFC3339 ),
236- }
237- webhookType = WebhookTypeTest
193+ }, WebhookTypeTest
238194 }
239-
240- body := WebhookStruct {
241- Organization : org ,
242- Project : project ,
243- Asset : asset ,
244- AssetVersion : assetVersion ,
245- Payload : payload ,
246- Type : webhookType ,
247- }
248-
249- var buf bytes.Buffer
250- err := json .NewEncoder (& buf ).Encode (body )
251- if err != nil {
252- return err
253- }
254-
255- resp , err := c .CreateRequest (ctx , "POST" , c .URL , & buf )
256- if err != nil {
257- return err
258- }
259- if resp == nil {
260- return fmt .Errorf ("received nil response when sending test webhook" )
261- }
262- defer resp .Body .Close ()
263- if resp .StatusCode >= 200 && resp .StatusCode < 300 {
264- return nil // Success
265- }
266-
267- return fmt .Errorf ("failed to send test webhook, status: %s" , resp .Status )
268195}
269196
270197func createSampleSBOM () cdx.BOM {
0 commit comments