diff --git a/.vscode/settings.json b/.vscode/settings.json
index 80cdf3b7bb..52f38406fa 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,6 +7,7 @@
"actioned",
"APIID",
"autometa",
+ "besteffort",
"bifactory",
"blobstore",
"blockchainids",
diff --git a/doc-site/docs/reference/types/_includes/subscription_description.md b/doc-site/docs/reference/types/_includes/subscription_description.md
index b83a74a1ab..262b71066d 100644
--- a/doc-site/docs/reference/types/_includes/subscription_description.md
+++ b/doc-site/docs/reference/types/_includes/subscription_description.md
@@ -171,10 +171,13 @@ allowing you to customize your HTTP requests as follows:
- To retry requests to your Webhook on a non-`2xx` HTTP status code
or other error, you should enable and configure
[options.retry](#webhookretryoptions)
- - The event is acknowledged once the request (with any retries), is
+ - By default the event is acknowledged once the request (with any retries), is
completed - regardless of whether the outcome was a success or failure.
+- Use `confirmationMode` to choose whether a delivery has to be confirmed before
+ the subscription checkpoint advances (see below).
- Use `fastack` to acknowledge against FireFly immediately and make multiple
- parallel calls to the HTTP API in a fire-and-forget fashion.
+ parallel calls to the HTTP API in a fire-and-forget fashion. **Deprecated**:
+ please use `confirmationMode: fastack` instead.
- Set the HTTP request details dynamically from `message_confirmed` events:
- Map data out of the first `data` element in message events
- Requires `withData` to be set on the subscription, in addition to the
@@ -185,6 +188,37 @@ allowing you to customize your HTTP requests as follows:
- Sets a `tag` in the reply message, per the configuration, or dynamically
based on a field in the input request data.
+#### Delivery confirmation
+
+The `confirmationMode` option determines whether - and when - a Webhook delivery
+causes the subscription checkpoint to advance:
+
+| `confirmationMode` | Behavior |
+| ------------------ | -------- |
+| `fastack` | Acknowledge each event *before* the delivery is attempted. The delivery runs detached, and its outcome never affects the checkpoint. Equivalent to the deprecated `fastack: true` option. |
+| `besteffort` | Deliver synchronously - holding the checkpoint while the invocation, including any configured [options.retry](#webhookretryoptions) attempts, completes - then acknowledge each event regardless of the response status or any error. **This is the default**, and the behavior of all releases before this option existed. |
+| `assured` | Deliver synchronously and acknowledge only on a `2xx` response. Anything else - including a connection failure - holds the subscription checkpoint, so the event(s) are redelivered. |
+
+Use `assured` when your application needs at-least-once semantics and would
+rather the subscription stall on a broken endpoint than silently skip past
+events it never received. Note that this means a persistently failing endpoint
+will block that subscription, so configure
+[options.retry](#webhookretryoptions) to suit, and monitor for a subscription
+that stops progressing.
+
+> Delivery confirmation does not apply when `reply` is enabled. In reply mode
+> the Webhook response - whatever its status - *is* the payload relayed back to
+> the original caller, so there is nothing to hold the checkpoint for.
+
+The `fastack` boolean is retained for backwards compatibility, but is
+deprecated in favor of `confirmationMode`:
+
+- `fastack: true` with no `confirmationMode` resolves to `confirmationMode: fastack`
+- Neither set resolves to `confirmationMode: besteffort` - today's default, unchanged
+- `fastack: true` combined with any `confirmationMode` other than `fastack` is
+ rejected when the subscription is created or updated, as the two are saying
+ contradictory things
+
#### Batching events
Webhooks have the ability to batch events into a single HTTP request instead of sending an event per HTTP request. The interface will be a JSON array of events instead of a top level JSON object with a single event. The size of the batch will be set by the `readAhead` limit and an optional timeout can be specified to send the events when the batch hasn't filled.
diff --git a/doc-site/docs/reference/types/subscription.md b/doc-site/docs/reference/types/subscription.md
index 903cf0d93f..5121928a88 100644
--- a/doc-site/docs/reference/types/subscription.md
+++ b/doc-site/docs/reference/types/subscription.md
@@ -90,7 +90,8 @@ title: Subscription
| `withData` | Whether message events delivered over the subscription, should be packaged with the full data of those messages in-line as part of the event JSON payload. Or if the application should make separate REST calls to download that data. May not be supported on some transports. | `bool` |
| `batch` | Events are delivered in batches in an ordered array. The batch size is capped to the readAhead limit. The event payload is always an array even if there is a single event in the batch, allowing client-side optimizations when processing the events in a group. Available for both Webhooks and WebSockets. | `bool` |
| `batchTimeout` | When batching is enabled, the optional timeout to send events even when the batch hasn't filled. | `string` |
-| `fastack` | Webhooks only: When true the event will be acknowledged before the webhook is invoked, allowing parallel invocations | `bool` |
+| `fastack` | Webhooks only: Deprecated - please use a confirmationMode of fastack instead. When true the event will be acknowledged before the webhook is invoked, allowing parallel invocations | `bool` |
+| `confirmationMode` | Webhooks only: Determines whether a webhook delivery must be confirmed before the subscription checkpoint advances. Use fastack to acknowledge the event before the webhook is invoked, besteffort (the default) to hold the checkpoint while the invocation - including any configured client-side retries - completes, then acknowledge regardless of its outcome, or assured to acknowledge only on a 2xx response and otherwise hold the checkpoint and redeliver | `FFEnum`:
`"fastack"`
`"besteffort"`
`"assured"` |
| `url` | Webhooks only: HTTP url to invoke. Can be relative if a base URL is set in the webhook plugin config | `string` |
| `method` | Webhooks only: HTTP method to invoke. Default=POST | `string` |
| `json` | Webhooks only: Whether to assume the response body is JSON, regardless of the returned Content-Type | `bool` |
diff --git a/doc-site/docs/reference/types/wsstart.md b/doc-site/docs/reference/types/wsstart.md
index 07e3f3223f..9e9b7143fa 100644
--- a/doc-site/docs/reference/types/wsstart.md
+++ b/doc-site/docs/reference/types/wsstart.md
@@ -81,7 +81,8 @@ title: WSStart
| `withData` | Whether message events delivered over the subscription, should be packaged with the full data of those messages in-line as part of the event JSON payload. Or if the application should make separate REST calls to download that data. May not be supported on some transports. | `bool` |
| `batch` | Events are delivered in batches in an ordered array. The batch size is capped to the readAhead limit. The event payload is always an array even if there is a single event in the batch, allowing client-side optimizations when processing the events in a group. Available for both Webhooks and WebSockets. | `bool` |
| `batchTimeout` | When batching is enabled, the optional timeout to send events even when the batch hasn't filled. | `string` |
-| `fastack` | Webhooks only: When true the event will be acknowledged before the webhook is invoked, allowing parallel invocations | `bool` |
+| `fastack` | Webhooks only: Deprecated - please use a confirmationMode of fastack instead. When true the event will be acknowledged before the webhook is invoked, allowing parallel invocations | `bool` |
+| `confirmationMode` | Webhooks only: Determines whether a webhook delivery must be confirmed before the subscription checkpoint advances. Use fastack to acknowledge the event before the webhook is invoked, besteffort (the default) to hold the checkpoint while the invocation - including any configured client-side retries - completes, then acknowledge regardless of its outcome, or assured to acknowledge only on a 2xx response and otherwise hold the checkpoint and redeliver | `FFEnum`:
`"fastack"`
`"besteffort"`
`"assured"` |
| `url` | Webhooks only: HTTP url to invoke. Can be relative if a base URL is set in the webhook plugin config | `string` |
| `method` | Webhooks only: HTTP method to invoke. Default=POST | `string` |
| `json` | Webhooks only: Whether to assume the response body is JSON, regardless of the returned Content-Type | `bool` |
diff --git a/doc-site/docs/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml
index 7e49130d7c..1f23fae086 100644
--- a/doc-site/docs/swagger/swagger.yaml
+++ b/doc-site/docs/swagger/swagger.yaml
@@ -31220,10 +31220,24 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before
+ the webhook is invoked, besteffort (the default) to hold
+ the checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be
- acknowledged before the webhook is invoked, allowing parallel
- invocations'
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
+ before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
description: Whether your application would like to receive
@@ -31513,8 +31527,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook delivery
+ must be confirmed before the subscription checkpoint advances.
+ Use fastack to acknowledge the event before the webhook is
+ invoked, besteffort (the default) to hold the checkpoint while
+ the invocation - including any configured client-side retries
+ - completes, then acknowledge regardless of its outcome, or
+ assured to acknowledge only on a 2xx response and otherwise
+ hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -31792,8 +31821,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before the
+ webhook is invoked, besteffort (the default) to hold the
+ checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -32081,8 +32125,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook delivery
+ must be confirmed before the subscription checkpoint advances.
+ Use fastack to acknowledge the event before the webhook is
+ invoked, besteffort (the default) to hold the checkpoint while
+ the invocation - including any configured client-side retries
+ - completes, then acknowledge regardless of its outcome, or
+ assured to acknowledge only on a 2xx response and otherwise
+ hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -32360,8 +32419,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before the
+ webhook is invoked, besteffort (the default) to hold the
+ checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -32712,8 +32786,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before the
+ webhook is invoked, besteffort (the default) to hold the
+ checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -40816,10 +40905,24 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before
+ the webhook is invoked, besteffort (the default) to hold
+ the checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be
- acknowledged before the webhook is invoked, allowing parallel
- invocations'
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
+ before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
description: Whether your application would like to receive
@@ -41102,8 +41205,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook delivery
+ must be confirmed before the subscription checkpoint advances.
+ Use fastack to acknowledge the event before the webhook is
+ invoked, besteffort (the default) to hold the checkpoint while
+ the invocation - including any configured client-side retries
+ - completes, then acknowledge regardless of its outcome, or
+ assured to acknowledge only on a 2xx response and otherwise
+ hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -41381,8 +41499,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before the
+ webhook is invoked, besteffort (the default) to hold the
+ checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -41663,8 +41796,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook delivery
+ must be confirmed before the subscription checkpoint advances.
+ Use fastack to acknowledge the event before the webhook is
+ invoked, besteffort (the default) to hold the checkpoint while
+ the invocation - including any configured client-side retries
+ - completes, then acknowledge regardless of its outcome, or
+ assured to acknowledge only on a 2xx response and otherwise
+ hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -41942,8 +42090,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before the
+ webhook is invoked, besteffort (the default) to hold the
+ checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
@@ -42280,8 +42443,23 @@ paths:
to send events even when the batch hasn't filled.
nullable: true
type: string
+ confirmationMode:
+ description: 'Webhooks only: Determines whether a webhook
+ delivery must be confirmed before the subscription checkpoint
+ advances. Use fastack to acknowledge the event before the
+ webhook is invoked, besteffort (the default) to hold the
+ checkpoint while the invocation - including any configured
+ client-side retries - completes, then acknowledge regardless
+ of its outcome, or assured to acknowledge only on a 2xx
+ response and otherwise hold the checkpoint and redeliver'
+ enum:
+ - fastack
+ - besteffort
+ - assured
+ type: string
fastack:
- description: 'Webhooks only: When true the event will be acknowledged
+ description: 'Webhooks only: Deprecated - please use a confirmationMode
+ of fastack instead. When true the event will be acknowledged
before the webhook is invoked, allowing parallel invocations'
type: boolean
firstEvent:
diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go
index 2370255994..1e4382abb1 100644
--- a/internal/coremsgs/en_error_messages.go
+++ b/internal/coremsgs/en_error_messages.go
@@ -324,4 +324,7 @@ var (
MsgInvalidCardanoAddress = ffe("FF10483", "Supplied cardano address is invalid", 400)
MsgCardanoconnectRESTErr = ffe("FF10484", "Error from cardano connector: %s")
MsgEmptyCustomTopic = ffe("FF10485", "Custom topic at index %d is empty. Each custom topic for the definition broadcast message must be a non-empty string", 400)
+ MsgWebhooksDeliveryFailedStatus = ffe("FF10486", "Webhook delivery failed with status %d", 502)
+ MsgWebhooksInvalidConfirmationMode = ffe("FF10487", "Webhook subscription option 'confirmationMode' value '%s' is invalid. Must be one of: %s", 400)
+ MsgWebhooksFastackConfirmationModeConflict = ffe("FF10488", "Webhook subscription option 'fastack' cannot be combined with 'confirmationMode' of '%s'. The deprecated 'fastack' option is equivalent to 'confirmationMode' of 'fastack'", 400)
)
diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go
index d6486ff6b1..b908f5bb2d 100644
--- a/internal/coremsgs/en_struct_descriptions.go
+++ b/internal/coremsgs/en_struct_descriptions.go
@@ -709,7 +709,8 @@ var (
WebhooksOptHeaders = ffm("WebhookSubOptions.headers", "Webhooks only: Static headers to set on the webhook request")
WebhooksOptQuery = ffm("WebhookSubOptions.query", "Webhooks only: Static query params to set on the webhook request")
WebhooksOptInput = ffm("WebhookSubOptions.input", "Webhooks only: A set of options to extract data from the first JSON input data in the incoming message. Only applies if withData=true")
- WebhooksOptFastAck = ffm("WebhookSubOptions.fastack", "Webhooks only: When true the event will be acknowledged before the webhook is invoked, allowing parallel invocations")
+ WebhooksOptFastAck = ffm("WebhookSubOptions.fastack", "Webhooks only: Deprecated - please use a confirmationMode of fastack instead. When true the event will be acknowledged before the webhook is invoked, allowing parallel invocations")
+ WebhooksOptConfirmationMode = ffm("WebhookSubOptions.confirmationMode", "Webhooks only: Determines whether a webhook delivery must be confirmed before the subscription checkpoint advances. Use fastack to acknowledge the event before the webhook is invoked, besteffort (the default) to hold the checkpoint while the invocation - including any configured client-side retries - completes, then acknowledge regardless of its outcome, or assured to acknowledge only on a 2xx response and otherwise hold the checkpoint and redeliver")
WebhooksOptURL = ffm("WebhookSubOptions.url", "Webhooks only: HTTP url to invoke. Can be relative if a base URL is set in the webhook plugin config")
WebhooksOptMethod = ffm("WebhookSubOptions.method", "Webhooks only: HTTP method to invoke. Default=POST")
WebhooksOptReplyTag = ffm("WebhookSubOptions.replytag", "Webhooks only: The tag to set on the reply message")
diff --git a/internal/events/webhooks/webhooks.go b/internal/events/webhooks/webhooks.go
index 781083ceae..b69704b895 100644
--- a/internal/events/webhooks/webhooks.go
+++ b/internal/events/webhooks/webhooks.go
@@ -263,7 +263,49 @@ func (wh *WebHooks) buildRequest(ctx context.Context, restyClient *resty.Client,
return req, err
}
+// confirmationModeFor resolves the effective delivery-confirmation mode for a subscription,
+// falling back to the deprecated `fastack` boolean for subscriptions created before
+// `confirmationMode` existed. Never returns an empty string - an unset option resolves to
+// "besteffort", which is the behavior of every release prior to this option existing.
+func confirmationModeFor(sub *core.Subscription) core.WebhookConfirmationMode {
+ opts := sub.Options.TransportOptions()
+ if mode := opts.GetString("confirmationMode"); mode != "" {
+ return core.WebhookConfirmationMode(strings.ToLower(mode))
+ }
+ if opts.GetBool("fastack") { // deprecated - retained for backwards compatibility only
+ return core.WebhookConfirmationModeFastAck
+ }
+ return core.WebhookConfirmationModeBestEffort // today's default, unchanged
+}
+
+func (wh *WebHooks) validateConfirmationMode(ctx context.Context, options *core.SubscriptionOptions) error {
+ transportOptions := options.TransportOptions()
+ rawMode := transportOptions.GetString("confirmationMode")
+ mode := core.WebhookConfirmationMode(strings.ToLower(rawMode))
+ if rawMode != "" && !fftypes.FFEnumValid(ctx, "webhookconfirmationmode", mode) {
+ validModes := make([]string, len(core.WebhookConfirmationModes))
+ for i, m := range core.WebhookConfirmationModes {
+ validModes[i] = string(m)
+ }
+ return i18n.NewError(ctx, coremsgs.MsgWebhooksInvalidConfirmationMode, rawMode, strings.Join(validModes, ", "))
+ }
+ // `fastack` is deprecated in favor of `confirmationMode`. It is fine on its own - that is how a
+ // subscription created before `confirmationMode` existed, with the deprecated boolean explicitly
+ // set, is configured (most existing subscriptions set neither, and resolve to "besteffort") - and
+ // fine alongside an explicit "fastack" mode.
+ // Any other combination is contradictory, and resolving it silently in either direction would
+ // surprise someone - so reject it at create/update time instead.
+ if transportOptions.GetBool("fastack") && rawMode != "" && mode != core.WebhookConfirmationModeFastAck {
+ return i18n.NewError(ctx, coremsgs.MsgWebhooksFastackConfirmationModeConflict, rawMode)
+ }
+ return nil
+}
+
func (wh *WebHooks) ValidateOptions(ctx context.Context, options *core.SubscriptionOptions) error {
+ if err := wh.validateConfirmationMode(ctx, options); err != nil {
+ return err
+ }
+
if options.WithData == nil {
defaultTrue := true
options.WithData = &defaultTrue
@@ -437,7 +479,14 @@ func (wh *WebHooks) attemptRequest(ctx context.Context, sub *core.Subscription,
return req, res, nil
}
-func (wh *WebHooks) doDelivery(ctx context.Context, connID string, reply bool, sub *core.Subscription, events []*core.CombinedEventDataDelivery, fastAck, batched bool) {
+// doDelivery performs the webhook invocation and emits the per-event delivery responses.
+//
+// It returns a non-nil error only in "assured" confirmation mode, on a non-2xx response. That
+// error propagates back through DeliveryRequest/BatchDeliveryRequest to the event dispatcher,
+// which holds the subscription checkpoint and redelivers rather than advancing past a delivery
+// that never landed. In every other mode - including "besteffort", the default - this returns
+// nil regardless of the response, exactly as it did before `confirmationMode` existed.
+func (wh *WebHooks) doDelivery(ctx context.Context, connID string, reply bool, sub *core.Subscription, events []*core.CombinedEventDataDelivery, fastAck, batched bool) error {
req, res, gwErr := wh.attemptRequest(ctx, sub, events, batched)
if gwErr != nil {
// Generate a bad-gateway error response - we always want to send something back,
@@ -457,6 +506,15 @@ func (wh *WebHooks) doDelivery(ctx context.Context, connID string, reply bool, s
b, _ := json.Marshal(&res)
log.L(ctx).Tracef("Webhook response: %s", string(b))
+ // Reply mode is exempt regardless of confirmation mode: the webhook response *is* the payload
+ // relayed back to the caller, so there is nothing to hold the checkpoint for.
+ if !reply && confirmationModeFor(sub) == core.WebhookConfirmationModeAssured &&
+ (res.Status < 200 || res.Status >= 300) {
+ log.L(ctx).Errorf("Webhook delivery returned status %d in '%s' confirmation mode - holding the subscription checkpoint and redelivering %d event(s)",
+ res.Status, core.WebhookConfirmationModeAssured, len(events))
+ return i18n.NewError(ctx, coremsgs.MsgWebhooksDeliveryFailedStatus, res.Status)
+ }
+
// For each event emit a response
for _, combinedEvent := range events {
event := combinedEvent.Event
@@ -500,6 +558,7 @@ func (wh *WebHooks) doDelivery(ctx context.Context, connID string, reply bool, s
}
}
+ return nil
}
func (wh *WebHooks) DeliveryRequest(ctx context.Context, connID string, sub *core.Subscription, event *core.EventDelivery, data core.DataArray) error {
@@ -523,7 +582,7 @@ func (wh *WebHooks) DeliveryRequest(ctx context.Context, connID string, sub *cor
// In fastack mode we drive calls in parallel to the backend, immediately acknowledging the event
// NOTE: We cannot use this with reply mode, as when we're sending a reply the `DeliveryResponse`
// callback must include the reply in-line.
- if !reply && sub.Options.TransportOptions().GetBool("fastack") {
+ if !reply && confirmationModeFor(sub) == core.WebhookConfirmationModeFastAck {
if cb, ok := wh.callbacks.handlers[sub.Namespace]; ok {
cb.DeliveryResponse(connID, &core.EventDeliveryResponse{
ID: event.ID,
@@ -531,15 +590,19 @@ func (wh *WebHooks) DeliveryRequest(ctx context.Context, connID string, sub *cor
Subscription: event.Subscription,
})
}
- go wh.doDelivery(ctx, connID, reply, sub, []*core.CombinedEventDataDelivery{{Event: event, Data: data}}, true, false)
+ go func() {
+ // The event is already acknowledged, so the outcome of this detached delivery cannot
+ // affect the subscription checkpoint. doDelivery never returns an error in this mode.
+ _ = wh.doDelivery(ctx, connID, reply, sub, []*core.CombinedEventDataDelivery{{Event: event, Data: data}}, true, false)
+ }()
return nil
}
// NOTE: We could check here for batching and accumulate but we can't return because this causes the offset to jump...
- // TODO we don't look at the error here?
- wh.doDelivery(ctx, connID, reply, sub, []*core.CombinedEventDataDelivery{{Event: event, Data: data}}, false, false)
- return nil
+ // A non-nil error here (only possible in "assured" mode) causes the dispatcher to hold the
+ // checkpoint and redeliver, rather than advancing past a failed delivery.
+ return wh.doDelivery(ctx, connID, reply, sub, []*core.CombinedEventDataDelivery{{Event: event, Data: data}}, false, false)
}
func (wh *WebHooks) BatchDeliveryRequest(ctx context.Context, connID string, sub *core.Subscription, events []*core.CombinedEventDataDelivery) error {
@@ -573,7 +636,7 @@ func (wh *WebHooks) BatchDeliveryRequest(ctx context.Context, connID string, sub
// // In fastack mode we drive calls in parallel to the backend, immediately acknowledging the event
// NOTE: We cannot use this with reply mode, as when we're sending a reply the `DeliveryResponse`
// callback must include the reply in-line.
- if !reply && sub.Options.TransportOptions().GetBool("fastack") {
+ if !reply && confirmationModeFor(sub) == core.WebhookConfirmationModeFastAck {
for _, combinedEvent := range events {
event := combinedEvent.Event
if cb, ok := wh.callbacks.handlers[sub.Namespace]; ok {
@@ -584,12 +647,17 @@ func (wh *WebHooks) BatchDeliveryRequest(ctx context.Context, connID string, sub
})
}
}
- go wh.doDelivery(ctx, connID, reply, sub, events, true, true)
+ go func() {
+ // The events are already acknowledged, so the outcome of this detached delivery cannot
+ // affect the subscription checkpoint. doDelivery never returns an error in this mode.
+ _ = wh.doDelivery(ctx, connID, reply, sub, events, true, true)
+ }()
return nil
}
- wh.doDelivery(ctx, connID, reply, sub, events, false, true)
- return nil
+ // A non-nil error here (only possible in "assured" mode) causes the dispatcher to hold the
+ // checkpoint and redeliver the whole batch, rather than advancing past a failed delivery.
+ return wh.doDelivery(ctx, connID, reply, sub, events, false, true)
}
func (wh *WebHooks) NamespaceRestarted(ns string, startTime time.Time) {
diff --git a/internal/events/webhooks/webhooks_test.go b/internal/events/webhooks/webhooks_test.go
index d2d931265e..021d116f45 100644
--- a/internal/events/webhooks/webhooks_test.go
+++ b/internal/events/webhooks/webhooks_test.go
@@ -28,11 +28,11 @@ import (
"fmt"
"log"
"math/big"
- "strings"
"net"
"net/http"
"net/http/httptest"
"os"
+ "strings"
"testing"
"time"
@@ -1603,3 +1603,364 @@ func TestLoggingContextPreserved(t *testing.T) {
mcb.AssertExpectations(t)
}
+
+// --- confirmationMode -------------------------------------------------------------------------
+//
+// See https://github.com/hyperledger-firefly/firefly/issues/1770. The critical property under
+// test is that nothing changes for a subscription that does not set `confirmationMode`, and that
+// only the new "assured" mode ever returns an error to the dispatcher (which is what holds the
+// subscription checkpoint and drives redelivery).
+
+// newConfirmationModeTestSub builds a webhook subscription pointing at a test server that always
+// responds with the given status, plus a single event to deliver over it.
+func newConfirmationModeTestSub(t *testing.T, status int) (sub *core.Subscription, event *core.EventDelivery, called *bool, closeServer func()) {
+ t.Helper()
+ wasCalled := false
+ r := mux.NewRouter()
+ r.HandleFunc("/myapi", func(res http.ResponseWriter, req *http.Request) {
+ wasCalled = true
+ res.WriteHeader(status)
+ _, _ = res.Write([]byte(`{}`))
+ }).Methods(http.MethodPost)
+ server := httptest.NewServer(r)
+
+ subID := fftypes.NewUUID()
+ sub = &core.Subscription{
+ SubscriptionRef: core.SubscriptionRef{
+ ID: subID,
+ Namespace: "ns1",
+ },
+ }
+ sub.Options.TransportOptions()["url"] = fmt.Sprintf("http://%s/myapi", server.Listener.Addr())
+ event = &core.EventDelivery{
+ EnrichedEvent: core.EnrichedEvent{
+ Event: core.Event{
+ ID: fftypes.NewUUID(),
+ Sequence: 12345,
+ },
+ Message: &core.Message{
+ Header: core.MessageHeader{
+ ID: fftypes.NewUUID(),
+ Type: core.MessageTypeBroadcast,
+ },
+ },
+ },
+ Subscription: core.SubscriptionRef{
+ ID: subID,
+ Namespace: "ns1",
+ },
+ }
+ return sub, event, &wasCalled, server.Close
+}
+
+func TestConfirmationModeForUnsetIsBestEffort(t *testing.T) {
+ sub := &core.Subscription{}
+ // Today's default, and the whole reason this is a non-breaking change
+ assert.Equal(t, core.WebhookConfirmationModeBestEffort, confirmationModeFor(sub))
+}
+
+func TestConfirmationModeForDeprecatedFastackFallback(t *testing.T) {
+ // The migration case: a subscription created before confirmationMode existed
+ sub := &core.Subscription{}
+ sub.Options.TransportOptions()["fastack"] = true
+ assert.Equal(t, core.WebhookConfirmationModeFastAck, confirmationModeFor(sub))
+
+ // An explicit false is not a signal of anything - still the default
+ sub2 := &core.Subscription{}
+ sub2.Options.TransportOptions()["fastack"] = false
+ assert.Equal(t, core.WebhookConfirmationModeBestEffort, confirmationModeFor(sub2))
+}
+
+func TestConfirmationModeForExplicitModes(t *testing.T) {
+ for _, mode := range core.WebhookConfirmationModes {
+ sub := &core.Subscription{}
+ sub.Options.TransportOptions()["confirmationMode"] = string(mode)
+ assert.Equal(t, mode, confirmationModeFor(sub))
+ }
+
+ // An explicit mode takes precedence over the deprecated boolean (only legal when they agree,
+ // per ValidateOptions, but resolution must not depend on that)
+ sub := &core.Subscription{}
+ sub.Options.TransportOptions()["fastack"] = true
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeFastAck)
+ assert.Equal(t, core.WebhookConfirmationModeFastAck, confirmationModeFor(sub))
+}
+
+func TestValidateOptionsConfirmationModeValid(t *testing.T) {
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ for _, mode := range append([]core.WebhookConfirmationMode{""}, core.WebhookConfirmationModes...) {
+ opts := &core.SubscriptionOptions{}
+ opts.TransportOptions()["url"] = "/anything"
+ if mode != "" {
+ opts.TransportOptions()["confirmationMode"] = string(mode)
+ }
+ err := wh.ValidateOptions(wh.ctx, opts)
+ assert.NoError(t, err, "confirmationMode '%s' should be accepted", mode)
+ }
+}
+
+func TestValidateOptionsConfirmationModeInvalid(t *testing.T) {
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ opts := &core.SubscriptionOptions{}
+ opts.TransportOptions()["url"] = "/anything"
+ opts.TransportOptions()["confirmationMode"] = "strict" // plausible typo, not a real mode
+ err := wh.ValidateOptions(wh.ctx, opts)
+ assert.Regexp(t, "FF10487", err)
+}
+
+func TestValidateOptionsFastackConfirmationModeConflict(t *testing.T) {
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ // fastack:true says "ack before delivery", the mode says otherwise - refuse to pick a winner
+ for _, mode := range []core.WebhookConfirmationMode{core.WebhookConfirmationModeBestEffort, core.WebhookConfirmationModeAssured} {
+ opts := &core.SubscriptionOptions{}
+ opts.TransportOptions()["url"] = "/anything"
+ opts.TransportOptions()["fastack"] = true
+ opts.TransportOptions()["confirmationMode"] = string(mode)
+ err := wh.ValidateOptions(wh.ctx, opts)
+ assert.Regexp(t, "FF10488", err, "fastack + confirmationMode '%s' should conflict", mode)
+ }
+}
+
+func TestValidateOptionsFastackConfirmationModeAgree(t *testing.T) {
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ // Saying the same thing twice is redundant, but not contradictory
+ opts := &core.SubscriptionOptions{}
+ opts.TransportOptions()["url"] = "/anything"
+ opts.TransportOptions()["fastack"] = true
+ opts.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeFastAck)
+ err := wh.ValidateOptions(wh.ctx, opts)
+ assert.NoError(t, err)
+
+ // As is the deprecated option on its own - every existing subscription looks like this
+ opts2 := &core.SubscriptionOptions{}
+ opts2.TransportOptions()["url"] = "/anything"
+ opts2.TransportOptions()["fastack"] = true
+ err = wh.ValidateOptions(wh.ctx, opts2)
+ assert.NoError(t, err)
+}
+
+func TestDeliveryRequestDefaultAcksOnNon2XX(t *testing.T) {
+ // No confirmationMode set: a 500 is still acknowledged and the checkpoint still advances.
+ // This is the no-regression test - if this one changes, the change is breaking.
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 500)
+ defer closeServer()
+
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+ mcb.On("DeliveryResponse", mock.Anything, mock.MatchedBy(func(response *core.EventDeliveryResponse) bool {
+ return !response.Rejected
+ })).Return(nil)
+
+ err := wh.DeliveryRequest(wh.ctx, mock.Anything, sub, event, core.DataArray{})
+ assert.NoError(t, err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestDeliveryRequestBestEffortAcksOnNon2XX(t *testing.T) {
+ // Naming today's behavior explicitly must behave identically to leaving it unset
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 500)
+ defer closeServer()
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeBestEffort)
+
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+ mcb.On("DeliveryResponse", mock.Anything, mock.MatchedBy(func(response *core.EventDeliveryResponse) bool {
+ return !response.Rejected
+ })).Return(nil)
+
+ err := wh.DeliveryRequest(wh.ctx, mock.Anything, sub, event, core.DataArray{})
+ assert.NoError(t, err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestDeliveryRequestAssuredHoldsCheckpointOnNon2XX(t *testing.T) {
+ // The new behavior: no ack at all, and an error back to the dispatcher, which nacks and
+ // rewinds the polling offset so these events are redelivered.
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 500)
+ defer closeServer()
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeAssured)
+
+ // Deliberately no DeliveryResponse expectation - any acknowledgement here is a failure,
+ // because the dispatcher is about to nack this event on the back of the error we return.
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+
+ err := wh.DeliveryRequest(wh.ctx, mock.Anything, sub, event, core.DataArray{})
+ assert.Regexp(t, "FF10486", err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestDeliveryRequestAssuredAcksOn2XX(t *testing.T) {
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 200)
+ defer closeServer()
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeAssured)
+
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+ mcb.On("DeliveryResponse", mock.Anything, mock.MatchedBy(func(response *core.EventDeliveryResponse) bool {
+ return !response.Rejected
+ })).Return(nil)
+
+ err := wh.DeliveryRequest(wh.ctx, mock.Anything, sub, event, core.DataArray{})
+ assert.NoError(t, err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestDeliveryRequestAssuredHoldsCheckpointOnNetworkFailure(t *testing.T) {
+ // A connection failure is synthesized into a 502 before it reaches the mode check, so it must
+ // hold the checkpoint too - nothing was delivered.
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, _, closeServer := newConfirmationModeTestSub(t, 200)
+ closeServer() // nothing is listening on that address any more
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeAssured)
+
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+
+ err := wh.DeliveryRequest(wh.ctx, mock.Anything, sub, event, core.DataArray{})
+ assert.Regexp(t, "FF10486", err)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestDeliveryRequestAssuredReplyModeStillAcksOnNon2XX(t *testing.T) {
+ // Reply mode is exempt from confirmationMode: the webhook response *is* the payload relayed
+ // back to the original caller, whatever its status, so there is nothing to hold open.
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 500)
+ defer closeServer()
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeAssured)
+ sub.Options.TransportOptions()["reply"] = true
+
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+ mcb.On("DeliveryResponse", mock.Anything, mock.MatchedBy(func(response *core.EventDeliveryResponse) bool {
+ assert.False(t, response.Rejected)
+ assert.Equal(t, float64(500), response.Reply.InlineData[0].Value.JSONObject()["status"])
+ return true
+ })).Return(nil)
+
+ err := wh.DeliveryRequest(wh.ctx, mock.Anything, sub, event, core.DataArray{})
+ assert.NoError(t, err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestDeliveryRequestConfirmationModeFastackIsDetached(t *testing.T) {
+ // confirmationMode: fastack must behave exactly like the deprecated boolean - ack up front,
+ // deliver detached, and never surface the outcome.
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, _, closeServer := newConfirmationModeTestSub(t, 500)
+ defer closeServer()
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeFastAck)
+
+ acked := make(chan struct{})
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+ mcb.On("DeliveryResponse", mock.Anything, mock.MatchedBy(func(response *core.EventDeliveryResponse) bool {
+ return !response.Rejected
+ })).Return(nil).Run(func(a mock.Arguments) {
+ close(acked)
+ })
+
+ err := wh.DeliveryRequest(wh.ctx, mock.Anything, sub, event, core.DataArray{})
+ assert.NoError(t, err)
+ <-acked
+
+ mcb.AssertExpectations(t)
+}
+
+func TestBatchDeliveryRequestDefaultAcksOnNon2XX(t *testing.T) {
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 500)
+ defer closeServer()
+
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+ mcb.On("DeliveryResponse", mock.Anything, mock.MatchedBy(func(response *core.EventDeliveryResponse) bool {
+ return !response.Rejected
+ })).Return(nil).Twice()
+
+ err := wh.BatchDeliveryRequest(wh.ctx, mock.Anything, sub, []*core.CombinedEventDataDelivery{
+ {Event: event, Data: core.DataArray{}},
+ {Event: event, Data: core.DataArray{}},
+ })
+ assert.NoError(t, err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestBatchDeliveryRequestAssuredHoldsCheckpointOnNon2XX(t *testing.T) {
+ // A failed batch nacks the whole batch, matching the existing batch nack semantics
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 500)
+ defer closeServer()
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeAssured)
+
+ // Again, no DeliveryResponse expectation - nothing in the batch may be acknowledged
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+
+ err := wh.BatchDeliveryRequest(wh.ctx, mock.Anything, sub, []*core.CombinedEventDataDelivery{
+ {Event: event, Data: core.DataArray{}},
+ {Event: event, Data: core.DataArray{}},
+ })
+ assert.Regexp(t, "FF10486", err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
+
+func TestBatchDeliveryRequestAssuredAcksOn2XX(t *testing.T) {
+ wh, cancel := newTestWebHooks(t)
+ defer cancel()
+
+ sub, event, called, closeServer := newConfirmationModeTestSub(t, 200)
+ defer closeServer()
+ sub.Options.TransportOptions()["confirmationMode"] = string(core.WebhookConfirmationModeAssured)
+
+ mcb := wh.callbacks.handlers["ns1"].(*eventsmocks.Callbacks)
+ mcb.On("DeliveryResponse", mock.Anything, mock.MatchedBy(func(response *core.EventDeliveryResponse) bool {
+ return !response.Rejected
+ })).Return(nil).Twice()
+
+ err := wh.BatchDeliveryRequest(wh.ctx, mock.Anything, sub, []*core.CombinedEventDataDelivery{
+ {Event: event, Data: core.DataArray{}},
+ {Event: event, Data: core.DataArray{}},
+ })
+ assert.NoError(t, err)
+ assert.True(t, *called)
+
+ mcb.AssertExpectations(t)
+}
diff --git a/pkg/core/webhooks.go b/pkg/core/webhooks.go
index 9036519d65..11e4fb9730 100644
--- a/pkg/core/webhooks.go
+++ b/pkg/core/webhooks.go
@@ -20,24 +20,60 @@ import (
"crypto/tls"
"github.com/go-resty/resty/v2"
+ "github.com/hyperledger-firefly/common/pkg/fftypes"
)
+// WebhookConfirmationMode is the delivery-confirmation mode for a webhook subscription, selected
+// via the `confirmationMode` option. These control whether - and when - a webhook delivery causes
+// the subscription checkpoint to advance. See WebhookSubOptions.ConfirmationMode.
+type WebhookConfirmationMode = fftypes.FFEnum
+
+var (
+ // WebhookConfirmationModeFastAck acknowledges each event before the delivery is attempted.
+ // The delivery runs detached, and its outcome never affects the subscription checkpoint.
+ // This is the behavior of the deprecated `fastack: true` option.
+ WebhookConfirmationModeFastAck = fftypes.FFEnumValue("webhookconfirmationmode", "fastack")
+ // WebhookConfirmationModeBestEffort delivers synchronously - holding the checkpoint while any
+ // client-side retries configured via WebhookRetryOptions run - then acknowledges each event once
+ // that completes, regardless of the response status or any error. This is the default when
+ // unset, and matches the behavior of all releases prior to the introduction of this option.
+ WebhookConfirmationModeBestEffort = fftypes.FFEnumValue("webhookconfirmationmode", "besteffort")
+ // WebhookConfirmationModeAssured delivers synchronously and only acknowledges on a 2xx
+ // response. Anything else holds the subscription checkpoint, so the event(s) are redelivered.
+ WebhookConfirmationModeAssured = fftypes.FFEnumValue("webhookconfirmationmode", "assured")
+)
+
+// WebhookConfirmationModes is the set of valid values for the `confirmationMode` option.
+var WebhookConfirmationModes = []WebhookConfirmationMode{
+ WebhookConfirmationModeFastAck,
+ WebhookConfirmationModeBestEffort,
+ WebhookConfirmationModeAssured,
+}
+
type WebhookSubOptions struct {
- Fastack bool `ffstruct:"WebhookSubOptions" json:"fastack,omitempty"`
- URL string `ffstruct:"WebhookSubOptions" json:"url,omitempty"`
- Method string `ffstruct:"WebhookSubOptions" json:"method,omitempty"`
- JSON bool `ffstruct:"WebhookSubOptions" json:"json,omitempty"`
- Reply bool `ffstruct:"WebhookSubOptions" json:"reply,omitempty"`
- ReplyTag string `ffstruct:"WebhookSubOptions" json:"replytag,omitempty"`
- ReplyTX string `ffstruct:"WebhookSubOptions" json:"replytx,omitempty"`
- Headers map[string]string `ffstruct:"WebhookSubOptions" json:"headers,omitempty"`
- Query map[string]string `ffstruct:"WebhookSubOptions" json:"query,omitempty"`
- TLSConfigName string `ffstruct:"WebhookSubOptions" json:"tlsConfigName,omitempty"`
- TLSConfig *tls.Config `ffstruct:"WebhookSubOptions" json:"-" ffexcludeinput:"true"`
- Input WebhookInputOptions `ffstruct:"WebhookSubOptions" json:"input,omitempty"`
- Retry WebhookRetryOptions `ffstruct:"WebhookSubOptions" json:"retry,omitempty"`
- HTTPOptions WebhookHTTPOptions `ffstruct:"WebhookSubOptions" json:"httpOptions,omitempty"`
- RestyClient *resty.Client `ffstruct:"WebhookSubOptions" json:"-" ffexcludeinput:"true"`
+ // DeprecatedFastack is retained for backwards compatibility with subscriptions created before
+ // ConfirmationMode existed. When ConfirmationMode is unset, `fastack: true` resolves to
+ // WebhookConfirmationModeFastAck. Setting both, with ConfirmationMode anything other than
+ // "fastack", is a validation error.
+ DeprecatedFastack bool `ffstruct:"WebhookSubOptions" json:"fastack,omitempty"`
+ // ConfirmationMode names the delivery-confirmation behavior explicitly. Defaults to
+ // WebhookConfirmationModeBestEffort when unset (and when unset with `fastack: true`,
+ // to WebhookConfirmationModeFastAck).
+ ConfirmationMode WebhookConfirmationMode `ffstruct:"WebhookSubOptions" json:"confirmationMode,omitempty" ffenum:"webhookconfirmationmode"`
+ URL string `ffstruct:"WebhookSubOptions" json:"url,omitempty"`
+ Method string `ffstruct:"WebhookSubOptions" json:"method,omitempty"`
+ JSON bool `ffstruct:"WebhookSubOptions" json:"json,omitempty"`
+ Reply bool `ffstruct:"WebhookSubOptions" json:"reply,omitempty"`
+ ReplyTag string `ffstruct:"WebhookSubOptions" json:"replytag,omitempty"`
+ ReplyTX string `ffstruct:"WebhookSubOptions" json:"replytx,omitempty"`
+ Headers map[string]string `ffstruct:"WebhookSubOptions" json:"headers,omitempty"`
+ Query map[string]string `ffstruct:"WebhookSubOptions" json:"query,omitempty"`
+ TLSConfigName string `ffstruct:"WebhookSubOptions" json:"tlsConfigName,omitempty"`
+ TLSConfig *tls.Config `ffstruct:"WebhookSubOptions" json:"-" ffexcludeinput:"true"`
+ Input WebhookInputOptions `ffstruct:"WebhookSubOptions" json:"input,omitempty"`
+ Retry WebhookRetryOptions `ffstruct:"WebhookSubOptions" json:"retry,omitempty"`
+ HTTPOptions WebhookHTTPOptions `ffstruct:"WebhookSubOptions" json:"httpOptions,omitempty"`
+ RestyClient *resty.Client `ffstruct:"WebhookSubOptions" json:"-" ffexcludeinput:"true"`
}
type WebhookRetryOptions struct {