Resubmit a stored event as a new undelivered event (closes #250)
All checks were successful
check / check (push) Successful in 3m3s
All checks were successful
check / check (push) Successful in 3m3s
Capturing real webhook traffic and firing it repeatedly at a backend under development is a primary function of this service, and per-delivery replay cannot do it: it only ever resolves the delivery's own original target, so a target created for a dev backend has no prior delivery and nothing can be replayed to it. The event log now offers a per-event Resubmit action. It stores a NEW event copying the stored one's method, headers, body and content type verbatim, and fans it out to the webhook's currently ACTIVE targets, resolved fresh by the query the receiver uses -- so a target created long after the original event arrived receives it. The original event's deliveries have no bearing on where the copy goes, inactive targets are skipped as the receiver skips them, and the action is repeatable: replay's in-flight refusal is deliberately not ported, because firing one captured event over and over is the point. The receiver and the resubmit path share one construction and one fan-out site. An eventSource value carries where the fields came from, live request or stored event, and createAndFanOut writes the event and its pending deliveries in one transaction and hands the tasks to the same Notifier, so a resubmitted delivery is retried, SSRF-guarded and circuit-broken exactly as a first one is. buildDeliveryTasks returns an error instead of writing a response, which is what lets both callers share it. The stored event is read once, before the write transaction, with a cast to blob, so a body over delivery.MaxInlineBodySize is copied byte for byte and the engine loads it from the new event row. A nullable resubmitted_from_id records provenance -- empty for an event that arrived on the receiver -- and the event log reports the relationship in both directions, without which the log is unreadable after a few resubmits of one event. The route sits in the owned-source group, so auth, CSRF and the body cap apply, with its own rate limit bucket and an events_resubmitted_total counter. Inbound signature verification is not re-run: there is no inbound signature to check on a copy an authenticated, CSRF-protected operator action submits. Per-delivery replay is unchanged; it serves recovery, which resubmit does not replace. The README claimed in four places that replay was unimplemented, one of them telling the operator that a delivery stranded by a target type change was lost; all four are corrected and resubmit is documented beside replay.
This commit is contained in:
58
README.md
58
README.md
@@ -903,10 +903,13 @@ webhooker solves this by acting as a durable intermediary:
|
||||
targets simultaneously. This enables patterns like forwarding a
|
||||
GitHub webhook to both a deployment service and a Slack channel.
|
||||
|
||||
5. **Replay** (not yet implemented) — Every received event is stored in
|
||||
full, which is what manual redelivery for debugging or testing will
|
||||
be built on. No redelivery exists today, in the web UI or the API;
|
||||
see [TODO.md](TODO.md).
|
||||
5. **Replay and resubmit** — Every received event is stored in full,
|
||||
and the event log offers two redelivery actions built on that.
|
||||
**Replay** re-sends one finished delivery to its own target, for
|
||||
recovering a delivery that failed. **Resubmit** re-injects the stored
|
||||
event as a new undelivered event and fans it out to every currently
|
||||
active target, for firing captured traffic at a backend under
|
||||
development. Both are web UI actions; there is no API for either.
|
||||
|
||||
### Use Cases
|
||||
|
||||
@@ -915,8 +918,8 @@ webhooker solves this by acting as a durable intermediary:
|
||||
- **Observability** via Prometheus metrics on webhook frequency, payload
|
||||
size, and delivery performance
|
||||
- **Debugging** and introspection of webhook payloads in the web UI
|
||||
- **Replay** of webhook events for application testing and development
|
||||
(planned; not yet implemented)
|
||||
- **Resubmit** of captured webhook events for application testing and
|
||||
development, and **replay** of a single failed delivery for recovery
|
||||
- **Fan-out** delivery of a single webhook to multiple downstream
|
||||
targets
|
||||
- **High-availability ingestion** for delivery to less reliable backend
|
||||
@@ -1224,7 +1227,7 @@ A programmatic access credential for API authentication.
|
||||
#### Event
|
||||
|
||||
A captured incoming webhook request. Stores the complete HTTP request
|
||||
data for auditing and for the planned replay capability.
|
||||
data for auditing, for replay, and for resubmission.
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------- | ------ | ----------- |
|
||||
@@ -1235,6 +1238,7 @@ data for auditing and for the planned replay capability.
|
||||
| `headers` | JSON | Complete request headers |
|
||||
| `body` | text | Raw request body |
|
||||
| `content_type` | string | Content-Type header value |
|
||||
| `resubmitted_from_id` | UUID | The event this one was copied from by a resubmit (nullable; empty for an event that arrived on the receiver). Not a foreign key: the source event can be reaped by retention while its copies remain |
|
||||
|
||||
**Relations:** Belongs to Webhook. Belongs to Entrypoint. Has many
|
||||
Deliveries.
|
||||
@@ -1283,6 +1287,30 @@ message on the event log rather than delivering from stale
|
||||
configuration, and a replay is refused while an earlier one for the
|
||||
same event and target is still pending or retrying.
|
||||
|
||||
**Resubmit.** Replay recovers one delivery; **resubmit** re-injects one
|
||||
EVENT. The event log offers a per-event **Resubmit** action that stores
|
||||
a NEW event copying the stored one's `method`, `headers`, `body` and
|
||||
`content_type` verbatim, then fans it out to the webhook's currently
|
||||
**active** targets — resolved fresh by the same query the receiver
|
||||
uses, so a target created long after the original event arrived
|
||||
receives it. That is the difference that matters: a target added to
|
||||
test a backend under development has no prior delivery, so there is
|
||||
nothing to replay to it, while a resubmit reaches it like any other
|
||||
active target. Inactive targets are skipped, exactly as the receiver
|
||||
skips them.
|
||||
|
||||
The new event is a first-class event in the log with its own
|
||||
deliveries, not a marker on the one it came from, and the original's
|
||||
deliveries are left untouched. It records `resubmitted_from_id`, and
|
||||
the event log shows the relationship both ways, so a captured event
|
||||
fired twenty times at a backend stays traceable. Resubmitting the same
|
||||
event repeatedly is supported and is the point of the action — there is
|
||||
no in-flight refusal; the route's own rate limit is what bounds it.
|
||||
|
||||
Inbound signature verification is not re-run on a resubmit. There is no
|
||||
inbound signature to check on a copy the operator submits, and the
|
||||
route is authenticated and CSRF-protected as an operator action.
|
||||
|
||||
#### DeliveryResult
|
||||
|
||||
The result of a single delivery attempt. Every attempt (including
|
||||
@@ -1539,10 +1567,11 @@ unknown) one while one of its deliveries is still `retrying`, both
|
||||
recovery paths above terminally mark that delivery `failed` and record a
|
||||
`DeliveryResult` naming the current target type as the reason, logging it
|
||||
at warn level. The delivery is not re-dispatched under the new type — the
|
||||
operator never asked for that delivery — and while the event itself
|
||||
remains stored in the per-webhook event database, there is no way to
|
||||
redeliver it: manual redelivery is planned, not implemented (see
|
||||
[TODO.md](TODO.md)).
|
||||
operator never asked for that delivery — and the event itself remains
|
||||
stored in the per-webhook event database. Nothing is lost: the delivery
|
||||
is `failed`, which is terminal, so the event log offers **Replay** on
|
||||
it to re-send it to that target under the new type, and **Resubmit** on
|
||||
the event to re-inject it to every currently active target.
|
||||
|
||||
### Circuit Breaker (HTTP and Slack Targets with Retries)
|
||||
|
||||
@@ -1615,13 +1644,16 @@ arriving and being stored, they are just not getting anywhere.
|
||||
| `webhooker_deliveries_failed_total` | counter | Deliveries that failed terminally and will not be retried |
|
||||
| `webhooker_delivery_retries_total` | counter | Deliveries put back into `retrying` |
|
||||
| `webhooker_delivery_replays_total` | counter | Deliveries an operator replayed from the event log. A replay runs the ordinary engine path, so it also moves the attempt, outcome and duration series; this is the only one that separates it from ordinary traffic |
|
||||
| `webhooker_events_resubmitted_total` | counter | Stored events an operator re-injected from the event log. The new event also moves `webhooker_events_received_total`, since it is a stored event the delivery side is compared against; this counter is what separates the two. Unlabelled: the target types it fans out to belong to the delivery series |
|
||||
| `webhooker_delivery_duration_seconds` | histogram | Wall time of a single dispatched delivery attempt, the same duration the attempt's `DeliveryResult` records |
|
||||
| `webhooker_deliveries_pending` | gauge | Deliveries currently in `pending` |
|
||||
| `webhooker_deliveries_retrying` | gauge | Deliveries currently in `retrying` |
|
||||
| `webhooker_circuit_breakers_open` | gauge | Circuit breakers currently open |
|
||||
|
||||
Every delivery metric carries exactly one label, `target_type`, and
|
||||
cardinality is the whole reason for that restriction. A target type is
|
||||
cardinality is the whole reason for that restriction. The two
|
||||
event-level counters carry no label at all — an event is not the
|
||||
property of any one target type. A target type is
|
||||
one of four compile-time constants, so the label domain is bounded by
|
||||
construction; a value outside that set collapses to `unknown` rather
|
||||
than minting a series of its own. Target ids, event ids and entrypoint
|
||||
@@ -2213,6 +2245,7 @@ abuse limit later; they are tracked as future work.
|
||||
| `POST` | `/source/{id}/delete` | Delete webhook |
|
||||
| `GET` | `/source/{id}/logs` | Webhook event logs |
|
||||
| `POST` | `/source/{id}/deliveries/{deliveryID}/replay` | Replay a finished delivery: creates a new delivery for the same event against the target's current configuration (30 per minute per bucket, then `429`) |
|
||||
| `POST` | `/source/{id}/events/{eventID}/resubmit` | Resubmit a stored event: creates a new event copying it and fans that out to every currently active target (30 per minute per bucket, then `429`) |
|
||||
| `POST` | `/source/{id}/entrypoints` | Add entrypoint to webhook |
|
||||
| `POST` | `/source/{id}/entrypoints/{entrypointID}/delete` | Delete an entrypoint |
|
||||
| `POST` | `/source/{id}/entrypoints/{entrypointID}/toggle` | Enable or disable an entrypoint |
|
||||
@@ -2303,6 +2336,7 @@ webhooker/
|
||||
│ │ ├── handlers.go # Base handler struct, JSON helpers, template rendering
|
||||
│ │ ├── auth.go # Login, logout handlers
|
||||
│ │ ├── delivery_replay.go # Per-delivery replay: new delivery, current target config
|
||||
│ │ ├── event_resubmit.go # Event resubmit: new event, all currently active targets
|
||||
│ │ ├── entrypoint_view.go # Masked entrypoint view for templates
|
||||
│ │ ├── event_log_view.go # Event log projection, byte-capped in SQL
|
||||
│ │ ├── healthcheck.go # Health check handler
|
||||
|
||||
Reference in New Issue
Block a user