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
+71
View File
@@ -1,13 +1,16 @@
package handlers_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -116,6 +119,7 @@ func TestEventRequest_EachEventShowsItsOwnEntrypointAndHeaders(
t.Helper()
assert.Contains(t, page, arrivedAt("Billing sender"))
assert.Contains(t, page, "No query string.")
assert.Contains(t, page, headerBox(
"Accept: */*",
"User-Agent: shop/1 build\t7",
@@ -331,3 +335,70 @@ func TestEventRequest_ManyShortHeaderLines(t *testing.T) {
})
}
}
// TestHandleWebhook_StoresAndShowsTheQueryString posts to an
// entrypoint's URL with a query string and proves the event stores it
// as sent, and shows it escaped in the event log and on its own page,
// in a box like the one the request headers show in.
func TestHandleWebhook_StoresAndShowsTheQueryString(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
ep := seedEntrypoint(t, f.db, f.webhook.ID)
req := httptest.NewRequestWithContext(
context.Background(), http.MethodPost,
"/h/"+ep.Path+"?a=1&b=2", strings.NewReader("{}"),
)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("uuid", ep.Path)
req = req.WithContext(context.WithValue(
req.Context(), chi.RouteCtxKey, rctx,
))
w := httptest.NewRecorder()
f.h.HandleWebhook().ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var stored database.Event
require.NoError(t, f.webhookDB.First(&stored).Error)
assert.Equal(t, "a=1&b=2", stored.RawQuery)
page := renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID)
assert.Contains(t, page, headerBox("a=1&b=2"))
w = serveEventPage(t, f.h, f.sess, f.webhook.ID, stored.ID)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), headerBox("a=1&b=2"))
}
// TestEventRequest_QueryStringOverTheLimit proves the event log leaves
// out a query string that holds more than it shows of a body, and links
// to the event's own page, which shows it whole.
func TestEventRequest_QueryStringOverTheLimit(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
ep := f.entrypoint(t, "Billing sender")
event := f.eventAt(t, ep, `{}`, time.Now())
query := "q=" + strings.Repeat("x", bodyCap)
require.NoError(t, f.webhookDB.Model(event).Update(
"raw_query", query,
).Error)
page := renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID)
assert.Contains(t, page, `<a href="/hook/`+f.webhook.ID+`/events/`+
event.ID+`" class="btn-small">Show the query string</a>`)
assert.NotContains(t, page, "q=x")
assert.Less(t, len(page), 4*bodyCap)
w := serveEventPage(t, f.h, f.sess, f.webhook.ID, event.ID)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), headerBox(query))
assert.NotContains(t, w.Body.String(), "Show the query string")
}