Render delivery attempt detail in the event log (closes #202)
All checks were successful
check / check (push) Successful in 3m35s
All checks were successful
check / check (push) Successful in 3m35s
Expanding a delivery on the event log page now shows each recorded attempt: attempt number, outcome, status code, duration, error and response body. Previously a failure rendered as "target: failed" and diagnosing it meant opening the per-webhook SQLite file by hand. The response body is cut by SQLite via substr over a blob cast, the same projection the event body uses, so an oversized stored response never becomes a Go string. The page reports the cut with a marker. Response bodies and errors are remote content, so both go through a new delivery.Redactor that strips the target's own destination URL, path, query and userinfo, plus the values of credential-shaped request headers, before rendering. Target configuration keeps reaching the template only as a TargetView. A body that reaches the cap is treated as cut whether or not SQLite is what cut it. The delivery engine stops reading a response at its own cap, which is the same number of bytes this page renders, and the row it writes records that cut length as the whole length, so nothing in the row separates a response that ended at the cap from one severed there. Such a body goes through RedactCut, which drops any tail that is a proper prefix of a secret: the remote chooses the padding in front of a credential it echoes, so it chooses where the cut falls inside that credential. Its marker says the response reached the recording limit rather than quoting a total the row does not know. Redactors are built from an unscoped target load. Deleting a target only soft deletes the row while its deliveries survive, and a scoped load would leave exactly those deliveries rendering unredacted. The views the page lists stay scoped. Attempt loading is chunked so the IN clause cannot exceed SQLite's bound-parameter limit, and a chunk that fails fails the page rather than rendering the deliveries it covered as never having run. The page renders at most 20 attempts per delivery, counting what it leaves out. static/css/tailwind.css is regenerated with tailwindcss for the utility classes the new markup uses.
This commit is contained in:
@@ -1201,6 +1201,83 @@ func TestDeliverHTTP_TargetTimeout(t *testing.T) {
|
||||
iAssertResultFailed(t, db, del.ID)
|
||||
}
|
||||
|
||||
// TestDeliverHTTP_CutsStoredResponseAtMaxBodyLog pins the size
|
||||
// this engine stores for an oversized response, because the
|
||||
// event log's redaction is written against it: the row holds
|
||||
// exactly maxBodyLog bytes and records nothing about how much
|
||||
// more the remote sent, so a credential echoed across that
|
||||
// boundary reaches the database already severed and no reader
|
||||
// of the row can tell the cut happened.
|
||||
func TestDeliverHTTP_CutsStoredResponseAtMaxBodyLog(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
// Padded so the cut falls five bytes before the end of the
|
||||
// echoed webhook URL.
|
||||
const (
|
||||
severedTail = 5
|
||||
overshoot = 100000
|
||||
)
|
||||
|
||||
sent := strings.Repeat(
|
||||
"A",
|
||||
delivery.ExportMaxBodyLog-len(slackWebhookURL)+
|
||||
severedTail,
|
||||
) + slackWebhookURL + strings.Repeat("Z", overshoot)
|
||||
|
||||
s := newISetup(t)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = io.WriteString(w, sent)
|
||||
},
|
||||
))
|
||||
defer ts.Close()
|
||||
|
||||
cfgJSON := iHTTPConfig(ts.URL)
|
||||
|
||||
event := iSeedEvent(
|
||||
t, s.WebhookDB, s.WebhookID, `{"cut":"test"}`,
|
||||
)
|
||||
targetID := uuid.New().String()
|
||||
|
||||
del := iSeedDelivery(
|
||||
t, s.WebhookDB, event.ID, targetID,
|
||||
database.DeliveryStatusPending,
|
||||
)
|
||||
|
||||
bodyStr := event.Body
|
||||
task := iTask(
|
||||
del, event, s.WebhookID, targetID,
|
||||
"cut-target", cfgJSON, 0, 1, &bodyStr,
|
||||
)
|
||||
|
||||
s.Engine.ExportProcessNewTask(context.TODO(), &task)
|
||||
|
||||
results := iResults(t, s.WebhookDB, del.ID)
|
||||
require.Len(t, results, 1)
|
||||
|
||||
stored := results[0].ResponseBody
|
||||
|
||||
assert.Len(
|
||||
t, stored, delivery.ExportMaxBodyLog,
|
||||
"an oversized response is stored at exactly the cap",
|
||||
)
|
||||
assert.Equal(
|
||||
t, sent[:delivery.ExportMaxBodyLog], stored,
|
||||
)
|
||||
assert.NotContains(
|
||||
t, stored, slackWebhookURL,
|
||||
"the echoed URL is severed by the cut",
|
||||
)
|
||||
assert.Contains(
|
||||
t, stored, "T00000000",
|
||||
"the severed prefix still carries the credential",
|
||||
)
|
||||
}
|
||||
|
||||
// iSeedEventAndDelivery creates event + delivery
|
||||
// for standalone tests.
|
||||
func iSeedEventAndDelivery(
|
||||
|
||||
Reference in New Issue
Block a user