Files
webhooker/internal/handlers/event_body_view.go
T
clawbot f346b69999
check / check (push) Successful in 3m15s
Give each event its own page and show bodies the same everywhere (closes #369)
Each row of the recent events on the webhook page links to the
event's own page, /hook/{id}/events/{eventID}, and expands to show its
body; only the newest starts expanded. The event's page shows its
details, its whole body and every delivery with its attempts.

One renderer, newBodyView with templates/event_body.html, shows a body
in all three places: whole up to 32 KiB, cut there in the lists with a
link to the event's page, JSON pretty-printed, a body of more than 200
lines or 32 KiB in a scrolling box, and a body that is not text left
out beside its download link. A resubmitted copy links to its
original's page.

Model: opus-5-5
2026-10-02 17:10:02 +00:00

148 lines
3.9 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"errors"
"io"
"unicode/utf8"
)
// maxRenderedBodyBytes is the most of one event's body that the
// recent events on a webhook's page and the event log show; a larger
// body is cut there and shown whole only on the event's own page.
// Bodies come from the unauthenticated receiver under its 1 MB cap,
// and renderTemplate buffers a whole page before writing it, so a list
// of events cannot show every body whole.
const maxRenderedBodyBytes = 32 << 10
// maxInlineBodyLines is the most lines a body is shown at its full
// height with. A body with more lines, or larger than
// maxRenderedBodyBytes, is shown in a box of fixed height that
// scrolls, so that it does not make the page huge.
const maxInlineBodyLines = 200
// maxIndentGrowth is how many times its size a JSON body may grow
// when it is pretty-printed; past that it is shown as received.
// Indenting grows with nesting depth as well as with size: 20 KB of
// nested brackets indents to some 200 MB.
const maxIndentGrowth = 4
// jsonIndent is the indent of a pretty-printed JSON body.
const jsonIndent = " "
// BodyView is an event's body as the pages show it. newBodyView
// decides it and templates/event_body.html shows it, the same way in
// the recent events on a webhook's page, in the event log and on the
// event's own page.
type BodyView struct {
// EventURL is the event's own page. The stored body downloads
// from EventURL/body.
EventURL string
// Text is the body as shown, pretty-printed when it is JSON.
Text string
// Size is the stored body's size in bytes, and ShownBytes how
// many of them Text holds when Cut.
Size int64
ShownBytes int
// Cut reports that Text is only the start of the body.
Cut bool
// Binary reports a body that is not text. It is not shown.
Binary bool
// Scroll reports a body to show in a box that scrolls.
Scroll bool
}
// newBodyView decides how to show an event's body. body is the
// stored body, or its first maxRenderedBodyBytes when only those were
// read, and size is the stored body's size.
func newBodyView(eventURL string, body []byte, size int64) BodyView {
v := BodyView{EventURL: eventURL, Size: size}
if size > int64(len(body)) {
v.Cut = true
body = trimPartialRune(body)
v.ShownBytes = len(body)
}
// html/template shows invalid UTF-8 and NUL bytes as replacement
// characters, so a body holding either is not text.
if !utf8.Valid(body) || bytes.IndexByte(body, 0) >= 0 {
v.Binary = true
return v
}
// A cut JSON document is no longer valid JSON.
if !v.Cut {
body = indentJSON(body)
}
lines := bytes.Count(body, []byte("\n")) + 1
v.Text = string(body)
v.Scroll = lines > maxInlineBodyLines || size > maxRenderedBodyBytes
return v
}
// indentJSON returns body pretty-printed when it is a JSON document,
// and unchanged when it is not or would grow more than
// maxIndentGrowth times.
func indentJSON(body []byte) []byte {
if !json.Valid(body) || !indentFits(body) {
return body
}
var out bytes.Buffer
err := json.Indent(&out, body, "", jsonIndent)
if err != nil {
return body
}
return out.Bytes()
}
// indentFits reports whether pretty-printing the JSON document body
// keeps it within maxIndentGrowth times its size. It adds up an upper
// bound instead of indenting: each token starts at most one line,
// indented once per enclosing object or array, and a key gains the
// space after its colon.
func indentFits(body []byte) bool {
limit := maxIndentGrowth * len(body)
size, depth := len(body), 0
dec := json.NewDecoder(bytes.NewReader(body))
// A number too large for a float64 is still valid JSON.
dec.UseNumber()
for size <= limit {
tok, err := dec.Token()
if errors.Is(err, io.EOF) {
return true
}
if err != nil {
return false
}
switch tok {
case json.Delim('{'), json.Delim('['):
depth++
case json.Delim('}'), json.Delim(']'):
depth--
}
size += len("\n") + depth*len(jsonIndent) + len(" ")
}
return false
}