Compare commits

1 Commits
Author SHA1 Message Date
sneak ce13e42eda Show an event's entrypoint and request headers (closes #389)
check / check (push) Successful in 3m13s
An expanded event in the event log and the event's own page now show
the entrypoint the event arrived at (its description, "Entrypoint"
when it has none, or "deleted entrypoint"; never its URL), or for a
resubmitted copy the one its original arrived at, and the request
headers, one per line, sorted by name, whitespace kept. The event log
leaves out headers that hold more than the body's 32 KiB limit, stored
or as lines, and links to the event's page, which shows them all.
Both pages draw them through one shared template, event_request.html,
and read the webhook's entrypoints once per page.

Model: opus-5-5
2026-10-03 02:29:51 +00:00
9 changed files with 452 additions and 18 deletions
+1 -1
View File
@@ -3104,7 +3104,7 @@ returns to the page that was asked for.
| `POST` | `/hook/{id}/edit` | Edit webhook submission | | `POST` | `/hook/{id}/edit` | Edit webhook submission |
| `POST` | `/hook/{id}/delete` | Delete webhook | | `POST` | `/hook/{id}/delete` | Delete webhook |
| `GET` | `/hook/{id}/events` | Full Event Log | | `GET` | `/hook/{id}/events` | Full Event Log |
| `GET` | `/hook/{id}/events/{eventID}` | One event's own page: its details, its whole body and every delivery of it | | `GET` | `/hook/{id}/events/{eventID}` | One event's own page: its details, the entrypoint it arrived at (for a resubmitted copy, the one its original arrived at), its request headers, its whole body and every delivery of it |
| `GET` | `/hook/{id}/events/{eventID}/body` | Download an event's stored body. The pages show a body as text, cut at 32 KiB in the recent events and the event log, and leave a binary one out, so this is the only route that serves the stored bytes; it is offered wherever a body is cut or binary | | `GET` | `/hook/{id}/events/{eventID}/body` | Download an event's stored body. The pages show a body as text, cut at 32 KiB in the recent events and the event log, and leave a binary one out, so this is the only route that serves the stored bytes; it is offered wherever a body is cut or binary |
| `POST` | `/hook/{id}/deliveries/{deliveryID}/replay` | Replay a finished delivery: creates a new delivery for the same event against the target's current configuration (30 per minute per bucket, then `429`) | | `POST` | `/hook/{id}/deliveries/{deliveryID}/replay` | Replay a finished delivery: creates a new delivery for the same event against the target's current configuration (30 per minute per bucket, then `429`) |
| `POST` | `/hook/{id}/events/{eventID}/resubmit` | Resubmit a stored event: creates a new event copying it and fans that out to every currently active target (30 per minute per bucket, then `429`) | | `POST` | `/hook/{id}/events/{eventID}/resubmit` | Resubmit a stored event: creates a new event copying it and fans that out to every currently active target (30 per minute per bucket, then `429`) |
+3 -1
View File
@@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"math"
"net/http" "net/http"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@@ -59,8 +60,9 @@ func (h *Handlers) HandleEventDetail() http.HandlerFunc {
return return
} }
// The page shows every request header.
views, ok := h.eventLogViews( views, ok := h.eventLogViews(
w, r, webhookDB, webhook.ID, rows, targets, w, r, webhookDB, webhook.ID, rows, targets, math.MaxInt,
) )
if !ok { if !ok {
return return
+111 -9
View File
@@ -1,10 +1,14 @@
package handlers package handlers
import ( import (
"encoding/json"
"net/http"
"slices"
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"sneak.berlin/go/webhooker/internal/database"
) )
// eventLogColumns is the event log's projection. The casts to // eventLogColumns is the event log's projection. The casts to
@@ -12,16 +16,20 @@ import (
// bytes rather than characters, so the cap bounds the page in // bytes rather than characters, so the cap bounds the page in
// bytes whatever the payload's encoding. Cutting in SQLite // bytes whatever the payload's encoding. Cutting in SQLite
// rather than in Go is the point of the projection — an // rather than in Go is the point of the projection — an
// oversized body never becomes a Go string at all. // oversized body or set of request headers never becomes a Go
// string at all.
const eventLogColumns = "id, created_at, method, content_type, " + const eventLogColumns = "id, created_at, method, content_type, " +
"resubmitted_from_id, " + "resubmitted_from_id, entrypoint_id, " +
"substr(cast(headers as blob), 1, ?) AS headers, " +
"length(cast(headers as blob)) AS headers_bytes, " +
"substr(cast(body as blob), 1, ?) AS body, " + "substr(cast(body as blob), 1, ?) AS body, " +
"length(cast(body as blob)) AS body_bytes" "length(cast(body as blob)) AS body_bytes"
// eventColumns is eventLogColumns for the event's own page, which // eventColumns is eventLogColumns for the event's own page, which
// shows the whole body. // shows the whole body and every request header.
const eventColumns = "id, created_at, method, content_type, " + const eventColumns = "id, created_at, method, content_type, " +
"resubmitted_from_id, " + "resubmitted_from_id, entrypoint_id, headers, " +
"length(cast(headers as blob)) AS headers_bytes, " +
"cast(body as blob) AS body, " + "cast(body as blob) AS body, " +
"length(cast(body as blob)) AS body_bytes" "length(cast(body as blob)) AS body_bytes"
@@ -40,6 +48,20 @@ type EventLogView struct {
Body BodyView Body BodyView
// Entrypoint names the entrypoint the event arrived at, or for a
// resubmitted copy, which did not arrive, the one its original
// arrived at: its description, "Entrypoint" when it has none, or
// "deleted entrypoint". Never its URL, which is the entrypoint's
// secret.
Entrypoint string
// Headers is the event's request headers, one "Name: value"
// per value, sorted by name. HeadersCut reports headers left
// out because they hold more than maxRenderedBodyBytes, stored
// or as lines; only the event log leaves them out.
Headers []string
HeadersCut bool
// ResubmittedFromID names the event this one was copied // ResubmittedFromID names the event this one was copied
// from, empty for an event that arrived on the receiver. // from, empty for an event that arrived on the receiver.
ResubmittedFromID string ResubmittedFromID string
@@ -60,27 +82,35 @@ func (v EventLogView) ResubmittedFrom() bool {
} }
// eventLogRow is one row of the event log projection, or of // eventLogRow is one row of the event log projection, or of
// eventColumns. In the event log its body column arrives // eventColumns. In the event log its headers and body columns
// already cut to the cap by SQLite, with the true size beside // arrive already cut to the cap by SQLite, each with its true
// it. // size beside it.
type eventLogRow struct { type eventLogRow struct {
ID string ID string
CreatedAt time.Time CreatedAt time.Time
Method string Method string
ContentType string ContentType string
ResubmittedFromID *string ResubmittedFromID *string
EntrypointID string
Headers string
HeadersBytes int64
Body []byte Body []byte
BodyBytes int64 BodyBytes int64
} }
// view projects a loaded row of the webhook's events for // view projects a loaded row of the webhook's events for
// rendering. // rendering. It shows the request headers when the row holds them
func (r *eventLogRow) view(webhookID string) EventLogView { // whole and their lines hold at most maxHeaderBytes.
func (r *eventLogRow) view(
webhookID string, maxHeaderBytes int,
) EventLogView {
var from string var from string
if r.ResubmittedFromID != nil { if r.ResubmittedFromID != nil {
from = *r.ResubmittedFromID from = *r.ResubmittedFromID
} }
headers, fit := requestHeaderLines(r.Headers, maxHeaderBytes)
return EventLogView{ return EventLogView{
ID: r.ID, ID: r.ID,
Method: r.Method, Method: r.Method,
@@ -90,10 +120,82 @@ func (r *eventLogRow) view(webhookID string) EventLogView {
Body: newBodyView( Body: newBodyView(
"/hook/"+webhookID+"/events/"+r.ID, r.Body, r.BodyBytes, "/hook/"+webhookID+"/events/"+r.ID, r.Body, r.BodyBytes,
), ),
Headers: headers,
HeadersCut: !fit || r.HeadersBytes > int64(len(r.Headers)),
ResubmittedFromID: from, ResubmittedFromID: from,
} }
} }
// requestHeaderLines turns an event's stored request headers, the
// JSON the receiver writes, into one "Name: value" line per value,
// sorted by name. Headers that do not parse, as when the event log
// has cut them, show as none. It reports false, with no lines, when
// the lines would hold more than maxBytes: a header sent many times
// is stored with its name once but shown with it on every line.
func requestHeaderLines(headersJSON string, maxBytes int) ([]string, bool) {
var headers http.Header
if json.Unmarshal([]byte(headersJSON), &headers) != nil {
return nil, true
}
names := make([]string, 0, len(headers))
for name := range headers {
names = append(names, name)
}
slices.Sort(names)
var lines []string
size := 0
for _, name := range names {
for _, value := range headers[name] {
line := name + ": " + value
size += len(line)
if size > maxBytes {
return nil, false
}
lines = append(lines, line)
}
}
return lines, true
}
// entrypointNames maps each of the webhook's entrypoints to the name
// an event that arrived at it shows: its description, or "Entrypoint"
// when it has none, as the webhook page names it. A deleted
// entrypoint is left out.
func (h *Handlers) entrypointNames(
webhookID string,
) (map[string]string, error) {
var entrypoints []database.Entrypoint
err := h.db.DB().Where(
"webhook_id = ?", webhookID,
).Find(&entrypoints).Error
if err != nil {
return nil, err
}
names := make(map[string]string, len(entrypoints))
for i := range entrypoints {
name := entrypoints[i].Description
if name == "" {
name = "Entrypoint"
}
names[entrypoints[i].ID] = name
}
return names, nil
}
// trimPartialRune drops a trailing UTF-8 sequence that the // trimPartialRune drops a trailing UTF-8 sequence that the
// byte-wise cut left incomplete, so a multi-byte rune severed // byte-wise cut left incomplete, so a multi-byte rune severed
// at the cap does not surface as a mojibake tail. // at the cap does not surface as a mojibake tail.
+276
View File
@@ -0,0 +1,276 @@
package handlers_test
import (
"encoding/json"
"net/http"
"slices"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm/clause"
"sneak.berlin/go/webhooker/internal/database"
)
// arrivedAt is how a page names the entrypoint an event arrived at.
func arrivedAt(name string) string {
return `Arrived at <span class="text-gray-900">` + name + `</span>`
}
// originalArrivedAt is how a page names the entrypoint a resubmitted
// copy's original arrived at.
func originalArrivedAt(name string) string {
return `Its original arrived at <span class="text-gray-900">` +
name + `</span>`
}
// headerLine is how a page shows one request header line.
func headerLine(line string) string {
return `<div class="whitespace-pre-wrap">` + line + `</div>`
}
// showHeadersLink is the event log's link to an event's own page for
// request headers it leaves out.
func showHeadersLink(webhookID, eventID string) string {
return `<a href="/hook/` + webhookID + `/events/` + eventID +
`" class="btn-small">Show the request headers</a>`
}
// entrypoint records one of the fixture webhook's entrypoints.
func (f *recentEventsFixture) entrypoint(
t *testing.T, description string,
) *database.Entrypoint {
t.Helper()
ep := &database.Entrypoint{
WebhookID: f.webhook.ID,
Path: uuid.NewString(),
Description: description,
Active: true,
}
require.NoError(t, f.db.DB().Omit(clause.Associations).Create(ep).Error)
return ep
}
// eventAt records an event that arrived at the entrypoint with the
// given request headers, stored as JSON as the receiver stores them.
func (f *recentEventsFixture) eventAt(
t *testing.T,
ep *database.Entrypoint,
headersJSON string,
receivedAt time.Time,
) *database.Event {
t.Helper()
event := &database.Event{
WebhookID: f.webhook.ID,
EntrypointID: ep.ID,
Method: http.MethodPost,
Headers: headersJSON,
Body: "{}",
BodyBytes: 2,
ContentType: contentTypeJSON,
}
event.CreatedAt = receivedAt
require.NoError(t, f.webhookDB.Omit(
clause.Associations,
).Create(event).Error)
return event
}
// TestEventRequest_EachEventShowsItsOwnEntrypointAndHeaders proves two
// events that arrived at two entrypoints each show their own
// entrypoint and request headers, in the event log and on their own
// pages, with the headers sorted by name, escaped and keeping their
// whitespace, and never the entrypoint's URL.
func TestEventRequest_EachEventShowsItsOwnEntrypointAndHeaders(
t *testing.T,
) {
t.Parallel()
f := newRecentEventsFixture(t)
billing := f.entrypoint(t, "Billing sender")
unnamed := f.entrypoint(t, "")
// Stored in reverse name order.
older := f.eventAt(t, billing,
`{"X-Shop-Event":["order.created"],`+
`"User-Agent":["shop/1 build\t7"],"Accept":["*/*"]}`,
time.Now().Add(-time.Minute))
newer := f.eventAt(t, unnamed,
`{"X-Shop-Event":["order.paid"],"X-Note":["<b>hi</b>"]}`,
time.Now())
olderShows := func(t *testing.T, page string) {
t.Helper()
const (
accept = "Accept: */*"
userAgent = "User-Agent: shop/1 build\t7"
shopEvent = "X-Shop-Event: order.created"
)
assert.Contains(t, page, arrivedAt("Billing sender"))
assert.Contains(t, page, headerLine(accept))
assert.Contains(t, page, headerLine(userAgent))
assert.Contains(t, page, headerLine(shopEvent))
assert.Less(t,
strings.Index(page, accept), strings.Index(page, userAgent),
"headers are sorted by name",
)
assert.Less(t,
strings.Index(page, userAgent), strings.Index(page, shopEvent),
"headers are sorted by name",
)
assert.NotContains(t, page, "order.paid")
assert.NotContains(t, page, billing.Path)
}
newerShows := func(t *testing.T, page string) {
t.Helper()
assert.Contains(t, page, arrivedAt("Entrypoint"))
assert.Contains(t, page, headerLine("X-Shop-Event: order.paid"))
assert.Contains(t, page, headerLine("X-Note: &lt;b&gt;hi&lt;/b&gt;"))
assert.NotContains(t, page, "<b>hi</b>")
assert.NotContains(t, page, "order.created")
assert.NotContains(t, page, unnamed.Path)
}
// The log lists the newer event first, so everything between
// the two events' first mentions belongs to the newer one.
_, rest, found := strings.Cut(renderSourceLogsPage(
t, f.h, f.sess, f.webhook.ID,
), newer.ID)
require.True(t, found)
newerPart, olderPart, found := strings.Cut(rest, older.ID)
require.True(t, found)
newerShows(t, newerPart)
olderShows(t, olderPart)
w := serveEventPage(t, f.h, f.sess, f.webhook.ID, newer.ID)
require.Equal(t, http.StatusOK, w.Code)
newerShows(t, w.Body.String())
w = serveEventPage(t, f.h, f.sess, f.webhook.ID, older.ID)
require.Equal(t, http.StatusOK, w.Code)
olderShows(t, w.Body.String())
}
// TestEventRequest_DeletedEntrypoint proves an event whose entrypoint
// has since been deleted says so in the event log and on its own page.
func TestEventRequest_DeletedEntrypoint(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
ep := f.entrypoint(t, "Retired sender")
event := f.eventAt(t, ep, `{}`, time.Now())
require.NoError(t, f.db.DB().Delete(ep).Error)
page := renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID)
assert.Contains(t, page, arrivedAt("deleted entrypoint"))
assert.NotContains(t, page, "Retired sender")
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(), arrivedAt("deleted entrypoint"))
assert.NotContains(t, w.Body.String(), "Retired sender")
}
// TestEventRequest_ResubmittedCopy proves a resubmitted copy says its
// original arrived at the entrypoint, in the event log and on its own
// page, and never that the copy did.
func TestEventRequest_ResubmittedCopy(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
ep := f.entrypoint(t, "Billing sender")
original := f.eventAt(t, ep, `{}`, time.Now().Add(-time.Minute))
copied := f.eventAt(t, ep, `{}`, time.Now())
require.NoError(t, f.webhookDB.Model(copied).Update(
"resubmitted_from_id", original.ID,
).Error)
// The log lists the copy first, and the original's Resubmit form
// comes after all of the copy and before the original's
// entrypoint.
copyPart, originalPart, found := strings.Cut(
renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID),
"/events/"+original.ID+"/resubmit",
)
require.True(t, found)
assert.Contains(t, copyPart, originalArrivedAt("Billing sender"))
assert.NotContains(t, copyPart, arrivedAt("Billing sender"))
assert.Contains(t, originalPart, arrivedAt("Billing sender"))
assert.NotContains(t, originalPart, originalArrivedAt("Billing sender"))
w := serveEventPage(t, f.h, f.sess, f.webhook.ID, copied.ID)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), originalArrivedAt("Billing sender"))
assert.NotContains(t, w.Body.String(), arrivedAt("Billing sender"))
}
// TestEventRequest_HeadersOverTheLimit proves the event log leaves out
// request headers that hold more than it shows of a body, whether
// stored or as lines, and links to the event's own page, which shows
// them all.
func TestEventRequest_HeadersOverTheLimit(t *testing.T) {
t.Parallel()
// The receiver stores each "<" as six bytes of JSON, so this
// header is over the limit stored but not as a line.
const lessThans = bodyCap/6 + 1
// A header sent many times is stored with its name once, and
// shown with it on every line.
repeatedName := "X-Repeated-" + strings.Repeat("r", 1000)
tests := map[string]struct {
headers http.Header
line string
}{
"stored": {
headers: http.Header{"X-Long": {strings.Repeat("<", lessThans)}},
line: "X-Long: " + strings.Repeat("&lt;", lessThans),
},
"as lines": {
headers: http.Header{repeatedName: slices.Repeat([]string{""}, 41)},
line: repeatedName + ": ",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
headersJSON, err := json.Marshal(tc.headers)
require.NoError(t, err)
f := newRecentEventsFixture(t)
ep := f.entrypoint(t, "Billing sender")
event := f.eventAt(t, ep, string(headersJSON), time.Now())
page := renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID)
assert.Contains(t, page, showHeadersLink(f.webhook.ID, event.ID))
assert.NotContains(t, page, tc.line)
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(), headerLine(tc.line))
assert.NotContains(t, w.Body.String(), "Show the request headers")
})
}
}
+4 -4
View File
@@ -167,12 +167,12 @@ func New(
), ),
"source_edit.html": parsePageTemplate("source_edit.html"), "source_edit.html": parsePageTemplate("source_edit.html"),
"source_logs.html": parsePageTemplate( "source_logs.html": parsePageTemplate(
"source_logs.html", "event_body.html", "delivery_row.html", "source_logs.html", "event_request.html", "event_body.html",
"delivery_attempts.html", "delivery_row.html", "delivery_attempts.html",
), ),
"event_detail.html": parsePageTemplate( "event_detail.html": parsePageTemplate(
"event_detail.html", "event_body.html", "delivery_row.html", "event_detail.html", "event_request.html", "event_body.html",
"delivery_attempts.html", "delivery_row.html", "delivery_attempts.html",
), ),
"target_edit.html": parsePageTemplate("target_edit.html"), "target_edit.html": parsePageTemplate("target_edit.html"),
"error.html": parsePageTemplate("error.html"), "error.html": parsePageTemplate("error.html"),
+21 -3
View File
@@ -1231,13 +1231,16 @@ func (h *Handlers) loadEventsWithDeliveries(
result, ok := h.eventLogViews( result, ok := h.eventLogViews(
w, r, webhookDB, webhook.ID, rows, targetMap, w, r, webhookDB, webhook.ID, rows, targetMap,
maxRenderedBodyBytes,
) )
return result, totalEvents, ok return result, totalEvents, ok
} }
// eventLogViews projects loaded events for rendering, each with // eventLogViews projects loaded events for rendering, each with
// its deliveries and how many times it has been resubmitted. Like // its deliveries, how many times it has been resubmitted and the
// entrypoint it arrived at, and with its request headers only when
// their lines hold at most maxHeaderBytes. Like
// loadEventsWithDeliveries, it reports false once it has answered // loadEventsWithDeliveries, it reports false once it has answered
// the request with an error. // the request with an error.
func (h *Handlers) eventLogViews( func (h *Handlers) eventLogViews(
@@ -1247,6 +1250,7 @@ func (h *Handlers) eventLogViews(
webhookID string, webhookID string,
rows []eventLogRow, rows []eventLogRow,
targetMap map[string]eventLogTarget, targetMap map[string]eventLogTarget,
maxHeaderBytes int,
) ([]EventLogView, bool) { ) ([]EventLogView, bool) {
result := make([]EventLogView, len(rows)) result := make([]EventLogView, len(rows))
eventDeliveries := make([][]database.Delivery, len(rows)) eventDeliveries := make([][]database.Delivery, len(rows))
@@ -1256,7 +1260,7 @@ func (h *Handlers) eventLogViews(
eventIDs := make([]string, len(rows)) eventIDs := make([]string, len(rows))
for i := range rows { for i := range rows {
result[i] = rows[i].view(webhookID) result[i] = rows[i].view(webhookID, maxHeaderBytes)
eventIDs[i] = rows[i].ID eventIDs[i] = rows[i].ID
webhookDB.Where( webhookDB.Where(
@@ -1290,11 +1294,25 @@ func (h *Handlers) eventLogViews(
return nil, false return nil, false
} }
entrypoints, err := h.entrypointNames(webhookID)
if err != nil {
h.serverError(w, r, "failed to load entrypoints", err)
return nil, false
}
for i := range rows { for i := range rows {
result[i].Deliveries = h.newDeliveryViews( result[i].Deliveries = h.newDeliveryViews(
eventDeliveries[i], targetMap, attempts, eventDeliveries[i], targetMap, attempts,
) )
result[i].ResubmitCount = resubmits[rows[i].ID] result[i].ResubmitCount = resubmits[rows[i].ID]
name, ok := entrypoints[rows[i].EntrypointID]
if !ok {
name = "deleted entrypoint"
}
result[i].Entrypoint = name
} }
return result, true return result, true
@@ -1315,7 +1333,7 @@ func loadEventLogRows(
var rows []eventLogRow var rows []eventLogRow
webhookDB.Model(&database.Event{}).Select( webhookDB.Model(&database.Event{}).Select(
eventLogColumns, maxRenderedBodyBytes, eventLogColumns, maxRenderedBodyBytes, maxRenderedBodyBytes,
).Where( ).Where(
"webhook_id = ?", webhookID, "webhook_id = ?", webhookID,
).Order("created_at DESC").Limit(recentEventLimit).Find(&rows) ).Order("created_at DESC").Limit(recentEventLimit).Find(&rows)
+9
View File
@@ -50,6 +50,15 @@
</dl> </dl>
</div> </div>
<div class="card mt-6">
<div class="p-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Request</h2>
</div>
<div class="p-4">
{{template "event_request" .}}
</div>
</div>
<div class="card mt-6"> <div class="card mt-6">
<div class="p-4 border-b border-gray-200"> <div class="p-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Body</h2> <h2 class="text-lg font-medium text-gray-900">Body</h2>
+24
View File
@@ -0,0 +1,24 @@
{{define "event_request"}}
<!-- The entrypoint an event arrived at and its request headers, as
handlers.EventLogView carries them: the same in the event log and
the event's own page. The entrypoint's URL is never shown. A
resubmitted copy did not arrive at an entrypoint; its original
did. -->
<div class="space-y-2 text-xs">
{{if .ResubmittedFrom}}
<p class="text-gray-500">Its original arrived at <span class="text-gray-900">{{.Entrypoint}}</span></p>
{{else}}
<p class="text-gray-500">Arrived at <span class="text-gray-900">{{.Entrypoint}}</span></p>
{{end}}
{{if .HeadersCut}}
<p class="text-gray-500">The request headers are larger than the event log shows. <a href="{{.Body.EventURL}}" class="btn-small">Show the request headers</a></p>
{{else if .Headers}}
<p class="text-gray-500">Request headers</p>
<div class="rounded-md border border-gray-200 bg-white p-2 font-mono text-gray-700 break-all">
{{range .Headers}}<div class="whitespace-pre-wrap">{{.}}</div>{{end}}
</div>
{{else}}
<p class="text-gray-500">No request headers.</p>
{{end}}
</div>
{{end}}
+3
View File
@@ -55,6 +55,9 @@
<button type="submit" class="btn-small" title="Submit this event again as a new event, to every currently active target">Resubmit</button> <button type="submit" class="btn-small" title="Submit this event again as a new event, to every currently active target">Resubmit</button>
</form> </form>
</div> </div>
<div class="mb-3">
{{template "event_request" .}}
</div>
{{template "event_body" .Body}} {{template "event_body" .Body}}
{{if .Deliveries}} {{if .Deliveries}}