check / check (push) Successful in 3m21s
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
169 lines
4.8 KiB
Go
169 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"unicode"
|
|
"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
|
|
|
|
// maxIndentDepth is how deeply a JSON body's objects and arrays may
|
|
// nest for it to be indented at all; a deeper one is shown as received.
|
|
// Each level indents every line inside it two more spaces, so 10 KB of
|
|
// nested brackets would indent to some 50 MB; within this depth a body
|
|
// grows at most 35 times.
|
|
const maxIndentDepth = 16
|
|
|
|
// A JSON body is shown pretty-printed only when that makes it at most
|
|
// maxIndentGrowth times its size plus indentAllowance bytes, and
|
|
// otherwise as received, so that indenting does not undo
|
|
// maxRenderedBodyBytes. The allowance keeps a small nested body
|
|
// pretty-printed.
|
|
const (
|
|
maxIndentGrowth = 4
|
|
indentAllowance = 1 << 10
|
|
)
|
|
|
|
// 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 as replacement characters,
|
|
// and a browser shows a control character other than tab, line
|
|
// feed and carriage return as a box or not at all, so a body
|
|
// holding either is not text.
|
|
isControl := func(r rune) bool {
|
|
return unicode.IsControl(r) && r != '\t' && r != '\n' && r != '\r'
|
|
}
|
|
|
|
if !utf8.Valid(body) || bytes.IndexFunc(body, isControl) >= 0 {
|
|
v.Binary = true
|
|
|
|
return v
|
|
}
|
|
|
|
// A cut JSON document is no longer valid JSON.
|
|
if !v.Cut {
|
|
body = indentJSON(body)
|
|
}
|
|
|
|
// The page shows a carriage return, a line feed, or the two
|
|
// together as one line break. A final one ends the last line
|
|
// rather than starting another.
|
|
text := bytes.TrimSuffix(body, []byte("\n"))
|
|
text = bytes.TrimSuffix(text, []byte("\r"))
|
|
breaks := bytes.Count(text, []byte("\n")) + bytes.Count(text, []byte("\r")) -
|
|
bytes.Count(text, []byte("\r\n"))
|
|
lines := breaks + 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, nests deeper than maxIndentDepth, or
|
|
// would grow past maxIndentGrowth times its size plus indentAllowance
|
|
// bytes.
|
|
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 || out.Len() > maxIndentGrowth*len(body)+indentAllowance {
|
|
return body
|
|
}
|
|
|
|
return out.Bytes()
|
|
}
|
|
|
|
// indentFits reports whether the objects and arrays of the JSON
|
|
// document body nest at most maxIndentDepth deep.
|
|
func indentFits(body []byte) bool {
|
|
depth := 0
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(body))
|
|
|
|
// A number too large for a float64 is still valid JSON.
|
|
dec.UseNumber()
|
|
|
|
for {
|
|
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++
|
|
if depth > maxIndentDepth {
|
|
return false
|
|
}
|
|
case json.Delim('}'), json.Delim(']'):
|
|
depth--
|
|
}
|
|
}
|
|
}
|