Repository: TBD54566975/ssi-service
Pull request #399 (2023-05-03 16:44:47)

Fixing context cancelled for webhook post calls
# Overview
Fixes an issue where we would get cancelled contexts for webhook calls


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/399?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#399](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/399?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (0be2975) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/44f90642cfbc76faa73f867ae6763288565b405d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (44f9064) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #399   +/-   ##
=======================================
  Coverage   20.43%   20.43%           
=======================================
  Files          48       48           
  Lines        5422     5422           
=======================================
  Hits         1108     1108           
  Misses       4139     4139           
  Partials      175      175           
```





Patch:
diff --git a/config/compose.toml b/config/compose.toml
index 092fef3..8fe7594 100644
--- a/config/compose.toml
+++ b/config/compose.toml
@@ -55,4 +55,11 @@ name = "schema"
 name = "credential"
 
 [services.manifest]
-name = "manifest"
\ No newline at end of file
+name = "manifest"
+
+[services.presentation]
+name = "presentation"
+
+[services.webhook]
+name = "webhook"
+webhook_timeout = "10s"
diff --git a/config/config.go b/config/config.go
index 1c18365..acd2b1d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -172,6 +172,7 @@ func (p *PresentationServiceConfig) IsEmpty() bool {
 
 type WebhookServiceConfig struct {
 	*BaseServiceConfig
+	WebhookTimeout string `toml:"webhook_timeout"`
 }
 
 func (p *WebhookServiceConfig) IsEmpty() bool {
@@ -283,6 +284,7 @@ func loadDefaultServicesConfig(config *SSIServiceConfig) {
 		},
 		WebhookConfig: WebhookServiceConfig{
 			BaseServiceConfig: &BaseServiceConfig{Name: "webhook"},
+			WebhookTimeout:    "10s",
 		},
 	}
 
diff --git a/config/config.toml b/config/config.toml
index aa76489..173a36f 100644
--- a/config/config.toml
+++ b/config/config.toml
@@ -70,3 +70,7 @@ name = "manifest"
 
 [services.presentation]
 name = "presentation"
+
+[services.webhook]
+name = "webhook"
+webhook_timeout = "10s"
diff --git a/config/config.toml.example b/config/config.toml.example
index e7e5ee2..28eded8 100644
--- a/config/config.toml.example
+++ b/config/config.toml.example
@@ -67,3 +67,7 @@ name = "manifest"
 
 [services.presentation]
 name = "presentation"
+
+[services.webhook]
+name = "webhook"
+webhook_timeout = "10s"
diff --git a/pkg/server/middleware/webhook.go b/pkg/server/middleware/webhook.go
index acac09e..0e16161 100644
--- a/pkg/server/middleware/webhook.go
+++ b/pkg/server/middleware/webhook.go
@@ -43,7 +43,7 @@ func Webhook(webhookService svcframework.Service, noun webhook.Noun, verb webhoo
 			err := handler(ctx, wrappedWriter, r)
 
 			whService := webhookService.(*webhook.Service)
-			go whService.PublishWebhook(ctx, noun, verb, buf)
+			go whService.PublishWebhook(noun, verb, buf)
 
 			return err
 		}
diff --git a/pkg/service/webhook/model.go b/pkg/service/webhook/model.go
index 0f5b186..b698e38 100644
--- a/pkg/service/webhook/model.go
+++ b/pkg/service/webhook/model.go
@@ -1,6 +1,9 @@
 package webhook
 
-import "net/url"
+import (
+	"encoding/json"
+	"net/url"
+)
 
 // In the context of webhooks, it's common to use noun.verb notation to describe events,
 // such as "credential.create" or "schema.delete".
@@ -31,10 +34,10 @@ type Webhook struct {
 }
 
 type Payload struct {
-	Noun Noun   `json:"noun" validate:"required"`
-	Verb Verb   `json:"verb" validate:"required"`
-	URL  string `json:"url" validate:"required"`
-	Data any    `json:"data,omitempty"`
+	Noun Noun            `json:"noun" validate:"required"`
+	Verb Verb            `json:"verb" validate:"required"`
+	URL  string          `json:"url" validate:"required"`
+	Data json.RawMessage `json:"data,omitempty"`
 }
 
 type CreateWebhookRequest struct {
diff --git a/pkg/service/webhook/service.go b/pkg/service/webhook/service.go
index 9894d73..1b6bcfb 100644
--- a/pkg/service/webhook/service.go
+++ b/pkg/service/webhook/service.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"time"
 
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
 	"github.com/goccy/go-json"
@@ -20,9 +21,10 @@ import (
 )
 
 type Service struct {
-	storage    *Storage
-	config     config.WebhookServiceConfig
-	httpClient *http.Client
+	storage         *Storage
+	config          config.WebhookServiceConfig
+	httpClient      *http.Client
+	timeoutDuration time.Duration
 }
 
 func (s Service) Type() framework.Type {
@@ -56,10 +58,16 @@ func NewWebhookService(config config.WebhookServiceConfig, s storage.ServiceStor
 
 	client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
 
+	duration, err := time.ParseDuration(config.WebhookTimeout)
+	if err != nil {
+		return nil, sdkutil.LoggingErrorMsg(err, "parsing webhook timeout")
+	}
+
 	service := Service{
-		storage:    webhookStorage,
-		config:     config,
-		httpClient: client,
+		storage:         webhookStorage,
+		config:          config,
+		httpClient:      client,
+		timeoutDuration: duration,
 	}
 
 	if !service.Status().IsReady() {
@@ -165,8 +173,11 @@ func (s Service) GetSupportedVerbs() GetSupportedVerbsResponse {
 	return GetSupportedVerbsResponse{Verbs: []Verb{Create, Delete}}
 }
 
-func (s Service) PublishWebhook(ctx context.Context, noun Noun, verb Verb, payloadReader io.Reader) {
-	webhook, err := s.storage.GetWebhook(context.Background(), string(noun), string(verb))
+func (s Service) PublishWebhook(noun Noun, verb Verb, payloadReader io.Reader) {
+	ctx, cancel := context.WithTimeout(context.Background(), s.timeoutDuration)
+	defer cancel()
+
+	webhook, err := s.storage.GetWebhook(ctx, string(noun), string(verb))
 	if err != nil {
 		logrus.WithError(err).Warn("get webhook")
 		return
@@ -182,7 +193,8 @@ func (s Service) PublishWebhook(ctx context.Context, noun Noun, verb Verb, paylo
 		return
 	}
 
-	postPayload := Payload{Noun: noun, Verb: verb, Data: string(payloadBytes)}
+	postPayload := Payload{Noun: noun, Verb: verb, Data: payloadBytes}
+
 	for _, url := range webhook.URLS {
 		postPayload.URL = url
 		postJSONData, err := json.Marshal(postPayload)
@@ -193,7 +205,7 @@ func (s Service) PublishWebhook(ctx context.Context, noun Noun, verb Verb, paylo
 
 		err = s.post(ctx, url, string(postJSONData))
 		if err != nil {
-			logrus.Warnf("posting payload to %s", url)
+			logrus.WithError(err).Warnf("posting payload to %s", url)
 		}
 	}
 }


Test patch:
diff --git a/pkg/server/router/webhook_test.go b/pkg/server/router/webhook_test.go
index 2e5dcfd..c4c6f36 100644
--- a/pkg/server/router/webhook_test.go
+++ b/pkg/server/router/webhook_test.go
@@ -29,7 +29,7 @@ func TestWebhookRouter(t *testing.T) {
 		db := setupTestDB(tt)
 		assert.NotNil(tt, db)
 
-		serviceConfig := config.WebhookServiceConfig{}
+		serviceConfig := config.WebhookServiceConfig{WebhookTimeout: "10s"}
 		webhookService, err := webhook.NewWebhookService(serviceConfig, db)
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, webhookService)
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index f0fa455..8017775 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -319,6 +319,7 @@ func testManifest(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Se
 func testWebhookService(t *testing.T, bolt storage.ServiceStorage) *webhook.Service {
 	serviceConfig := config.WebhookServiceConfig{
 		BaseServiceConfig: &config.BaseServiceConfig{Name: "webhook"},
+		WebhookTimeout:    "10s",
 	}
 
 	// create a webhook service