Store and show an event's query string, and pass it on to an HTTP target when set (closes #312)
check / check (push) Successful in 4m45s

The receiver dropped the query string of every request it received, so a sender's URL parameters were silently lost. Each event now keeps it, as sent, in a new `raw_query` column of the per-webhook `events` table; a resubmitted copy carries its original's. The event log and the event's page show it in the shared request block, the event log leaving out one over 32 KiB with a link, as for headers. The archive and log targets carry it. HTTP targets gain "Pass the query string on to this target", off by default: on, deliveries, replays and resubmits append it to the target URL, joined with `&` to one already there. The access log still hides it.

Model: opus-5-5
This commit was merged in pull request #501.
This commit is contained in:
2026-10-04 03:00:34 +02:00
parent a8fc0c5d32
commit ea8cba7264
32 changed files with 537 additions and 69 deletions
+29 -7
View File
@@ -17,19 +17,23 @@ import (
// bytes rather than characters, so the cap bounds the page in
// bytes whatever the payload's encoding. Cutting in SQLite
// rather than in Go is the point of the projection — an
// oversized body or set of request headers never becomes a Go
// string at all.
// oversized body, query string or set of request headers never
// becomes a Go string at all.
const eventLogColumns = "id, created_at, method, content_type, " +
"resubmitted_from_id, entrypoint_id, " +
"substr(cast(raw_query as blob), 1, ?) AS raw_query, " +
"length(cast(raw_query as blob)) AS raw_query_bytes, " +
"substr(cast(headers as blob), 1, ?) AS headers, " +
"length(cast(headers as blob)) AS headers_bytes, " +
"substr(cast(body as blob), 1, ?) AS body, " +
"length(cast(body as blob)) AS body_bytes"
// eventColumns is eventLogColumns for the event's own page, which
// shows the whole body and every request header.
// shows the whole body, the whole query string and every request
// header.
const eventColumns = "id, created_at, method, content_type, " +
"resubmitted_from_id, entrypoint_id, headers, " +
"resubmitted_from_id, entrypoint_id, raw_query, " +
"length(cast(raw_query as blob)) AS raw_query_bytes, headers, " +
"length(cast(headers as blob)) AS headers_bytes, " +
"cast(body as blob) AS body, " +
"length(cast(body as blob)) AS body_bytes"
@@ -57,6 +61,13 @@ type EventLogView struct {
// entrypoint's secret.
Entrypoint string
// RawQuery is the query string the event arrived with.
// RawQueryCut reports one left out, RawQuery then empty, because
// it holds more than maxRenderedBodyBytes; only the event log
// leaves it out.
RawQuery string
RawQueryCut bool
// Headers is the event's request headers as text, one
// "Name: value" line per value, sorted by name. HeadersCut
// reports headers left out because they hold more than
@@ -85,9 +96,9 @@ func (v EventLogView) ResubmittedFrom() bool {
}
// eventLogRow is one row of the event log projection, or of
// eventColumns. In the event log its headers and body columns
// arrive already cut to the cap by SQLite, each with its true
// size beside it.
// eventColumns. In the event log its query string, headers and
// body columns arrive already cut to the cap by SQLite, each with
// its true size beside it.
type eventLogRow struct {
ID string
CreatedAt time.Time
@@ -95,6 +106,8 @@ type eventLogRow struct {
ContentType string
ResubmittedFromID *string
EntrypointID string
RawQuery string
RawQueryBytes int64
Headers string
HeadersBytes int64
Body []byte
@@ -114,6 +127,13 @@ func (r *eventLogRow) view(
headers, fit := requestHeaderLines(r.Headers, maxHeaderBytes)
rawQuery := r.RawQuery
rawQueryCut := r.RawQueryBytes > int64(len(rawQuery))
if rawQueryCut {
rawQuery = ""
}
return EventLogView{
ID: r.ID,
Method: r.Method,
@@ -123,6 +143,8 @@ func (r *eventLogRow) view(
Body: newBodyView(
"/hook/"+webhookID+"/events/"+r.ID, r.Body, r.BodyBytes,
),
RawQuery: rawQuery,
RawQueryCut: rawQueryCut,
Headers: strings.Join(headers, "\n"),
HeadersCut: !fit || r.HeadersBytes > int64(len(r.Headers)),
ResubmittedFromID: from,