All checks were successful
check / check (push) Successful in 6m0s
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 before rendering. Configured HTTP header values are deliberately not redacted; they are as often routine as secret, and replacing them would mangle ordinary responses. Target configuration keeps reaching the template only as a TargetView.
127 lines
4.2 KiB
Go
127 lines
4.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
// maxRenderedResponseBytes caps how many bytes of one stored
|
|
// delivery response body reach the event log page.
|
|
//
|
|
// It matches the cap the delivery engine applies when it
|
|
// records a result, so nothing written by the current engine
|
|
// is cut twice. The bound is enforced here anyway, and in
|
|
// SQL: this page's memory profile must not depend on a
|
|
// constant in another package staying where it is, and rows
|
|
// predating that cap or restored from an archive are not
|
|
// covered by it at all.
|
|
const maxRenderedResponseBytes = 4096
|
|
|
|
// deliveryResultColumns is the delivery attempt projection.
|
|
// The casts to blob are load-bearing for the same reason they
|
|
// are in eventLogColumns: they make substr and length count
|
|
// bytes rather than characters, and they make SQLite do the
|
|
// cut, so an oversized stored response never becomes a Go
|
|
// string at all.
|
|
const deliveryResultColumns = "delivery_id, attempt_num, success, " +
|
|
"status_code, error, duration, " +
|
|
"substr(cast(response_body as blob), 1, ?) AS response_body, " +
|
|
"length(cast(response_body as blob)) AS response_bytes"
|
|
|
|
// DeliveryResultView is the display-safe projection of one
|
|
// delivery attempt for the event log page. It carries a
|
|
// capped response body plus the true stored size, so the page
|
|
// can mark a response as truncated without holding the whole
|
|
// thing.
|
|
//
|
|
// Both Error and ResponseBody have been through the target's
|
|
// Redactor. The engine already masks the URL out of the
|
|
// errors it stores, so for errors this is a second line
|
|
// covering rows written before it did; for response bodies it
|
|
// is the only line, and its reach is what
|
|
// delivery.Redactor documents.
|
|
type DeliveryResultView struct {
|
|
AttemptNum int
|
|
Success bool
|
|
|
|
// StatusCode is 0 when the attempt never got a response,
|
|
// which is why the page asks HasStatusCode rather than
|
|
// printing the number.
|
|
StatusCode int
|
|
|
|
// Error is the stored failure message, redacted.
|
|
Error string
|
|
|
|
// DurationMS is how long the attempt took.
|
|
DurationMS int64
|
|
|
|
// ResponseBody holds at most maxRenderedResponseBytes
|
|
// bytes of the stored response, redacted. It is remote
|
|
// content and must only ever be rendered escaped.
|
|
ResponseBody string
|
|
|
|
// ResponseBytes is the true size of the stored response
|
|
// body, before the cut and before redaction.
|
|
ResponseBytes int64
|
|
|
|
// ResponseShownBytes is how much of that the page is
|
|
// showing. It is the size of the cut, taken before
|
|
// redaction, so the truncation marker reports what SQLite
|
|
// returned rather than how much the marker substitution
|
|
// then changed the length.
|
|
ResponseShownBytes int
|
|
|
|
// ResponseTruncated reports that the stored response was
|
|
// larger than the cap, so the page owes the reader a
|
|
// marker.
|
|
ResponseTruncated bool
|
|
}
|
|
|
|
// HasStatusCode reports whether the attempt got as far as an
|
|
// HTTP response. A transport failure stores no status code,
|
|
// and rendering that as "0" would read as a real status.
|
|
func (v DeliveryResultView) HasStatusCode() bool {
|
|
return v.StatusCode != 0
|
|
}
|
|
|
|
// deliveryResultRow is one row of the delivery attempt
|
|
// projection. Its response body arrives already cut to the
|
|
// cap by SQLite, with the true size beside it.
|
|
type deliveryResultRow struct {
|
|
DeliveryID string
|
|
AttemptNum int
|
|
Success bool
|
|
StatusCode int
|
|
Error string
|
|
Duration int64
|
|
ResponseBody []byte
|
|
ResponseBytes int64
|
|
}
|
|
|
|
// view projects a loaded row for rendering, stripping the
|
|
// target's own credential out of the two fields a remote peer
|
|
// gets to influence.
|
|
func (r *deliveryResultRow) view(
|
|
redactor delivery.Redactor,
|
|
) DeliveryResultView {
|
|
body := r.ResponseBody
|
|
truncated := r.ResponseBytes > int64(len(body))
|
|
|
|
// Only a cut response can have been left mid-sequence by
|
|
// this query, exactly as with an event body.
|
|
if truncated {
|
|
body = trimPartialRune(body)
|
|
}
|
|
|
|
return DeliveryResultView{
|
|
AttemptNum: r.AttemptNum,
|
|
Success: r.Success,
|
|
StatusCode: r.StatusCode,
|
|
Error: redactor.Redact(r.Error),
|
|
DurationMS: r.Duration,
|
|
ResponseBody: redactor.Redact(string(body)),
|
|
ResponseBytes: r.ResponseBytes,
|
|
ResponseShownBytes: len(body),
|
|
ResponseTruncated: truncated,
|
|
}
|
|
}
|