Bound the event log's rendered bodies in the query (closes #135)
All checks were successful
check / check (push) Successful in 3m0s

The event log rendered stored bodies untruncated. Since buffered
rendering landed (#123) that became resident memory per concurrent
viewer, up to tens of MB, driven by payloads unauthenticated clients
supply to the public receiver.

Bound in the query rather than the template, via
substr(cast(body as blob), 1, ?) plus length(cast(body as blob)), so an
oversized body never becomes a Go string at all. Adds an EventLogView
projection carrying the true byte count, and trims a partial UTF-8 tail
without rewriting bodies that are merely invalid UTF-8.

Independently reviewed. The generated SQL was dumped under GORM DryRun
to confirm the cap is a bound parameter, both casts are present, and no
other path selects the full column; soft-delete scope, ordering and
pagination are unchanged.

Correction to the PR body: its quoted mutation output was produced by
removing the bound from eventLogColumns, not by raising the cap to
1<<30 as the text claimed. The reviewer reproduced the real
mutation and confirmed the tests do catch removal of the bound.

Follow-up #157 restores in-app retrieval of bodies above the cap.
This commit was merged in pull request #158.
This commit is contained in:
2026-08-17 22:57:08 +02:00
parent 9ae19159a3
commit 279effb4c2
6 changed files with 426 additions and 19 deletions

View File

@@ -92,13 +92,6 @@ func parseRetentionDays(raw string, fallback int) (int, error) {
return v, nil
}
// EventWithDeliveries holds an event and its deliveries.
type EventWithDeliveries struct {
database.Event
Deliveries []DeliveryView
}
// DeliveryView is the display-safe projection of a delivery
// for the event log page. Its target is a TargetView, so the
// stored configuration blob — which holds the target's
@@ -815,16 +808,18 @@ func (h *Handlers) parsePage(r *http.Request) int {
}
// loadEventsWithDeliveries loads paginated events and their
// deliveries from the per-webhook database.
// deliveries from the per-webhook database. Events come back
// as capped projections rather than database.Event rows: see
// eventLogColumns for why the cut happens in SQL.
func (h *Handlers) loadEventsWithDeliveries(
w http.ResponseWriter,
webhook database.Webhook,
targetMap map[string]delivery.TargetView,
page int,
) ([]EventWithDeliveries, int64) {
) ([]EventLogView, int64) {
var totalEvents int64
var result []EventWithDeliveries
var result []EventLogView
if !h.dbMgr.DBExists(webhook.ID) {
return result, totalEvents
@@ -845,23 +840,25 @@ func (h *Handlers) loadEventsWithDeliveries(
offset := (page - 1) * paginationPerPage
var events []database.Event
var rows []eventLogRow
webhookDB.Where(
webhookDB.Model(&database.Event{}).Select(
eventLogColumns, maxRenderedBodyBytes,
).Where(
"webhook_id = ?", webhook.ID,
).Order("created_at DESC").Offset(offset).Limit(
paginationPerPage,
).Find(&events)
).Find(&rows)
result = make([]EventWithDeliveries, len(events))
result = make([]EventLogView, len(rows))
for i := range events {
result[i].Event = events[i]
for i := range rows {
result[i] = rows[i].view()
var deliveries []database.Delivery
webhookDB.Where(
"event_id = ?", events[i].ID,
"event_id = ?", rows[i].ID,
).Find(&deliveries)
result[i].Deliveries = newDeliveryViews(