check / check (push) Successful in 3m17s
A refused save on the target edit page answered with a bare text page, losing the form and everything typed, and the webhook edit page came back with the stored values instead of the submitted ones. A refused target edit now shows the edit form again with the reason above it and every value submitted, with the same status codes as before; a refused webhook edit keeps the submitted name, description and retention. Target edits use the same validation as new targets, with no second copy; an encoding or database failure stays a logged 500. The browser test covers a refused save on both pages, and its main function is now a plain list of checks. Model: opus-5-5
91 lines
3.1 KiB
Go
91 lines
3.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// maxTargetRetries bounds a target's max_retries.
|
|
//
|
|
// Both target forms already declare max="20" on the input, so this
|
|
// enforces server-side what the UI has always advertised rather than
|
|
// introducing a new limit.
|
|
//
|
|
// The number is not cosmetic. Every attempt writes a delivery_results
|
|
// row that the event log then loads and renders, and the engine backs
|
|
// off by 2^(n-1) seconds, so attempt 20 is already about six days
|
|
// after the first. A value beyond this buys no additional durability
|
|
// and only costs rows.
|
|
const maxTargetRetries = 20
|
|
|
|
// Errors returned when a max_retries form value cannot be turned into
|
|
// a retry count.
|
|
var (
|
|
// errRetriesInvalid signals a max_retries form value that is not
|
|
// a non-negative whole number.
|
|
errRetriesInvalid = errors.New(
|
|
"retries must be a whole number of attempts",
|
|
)
|
|
|
|
// errRetriesTooLarge signals a max_retries form value that is a
|
|
// whole number but above maxTargetRetries. It is distinguished
|
|
// from errRetriesInvalid so the message can name the ceiling
|
|
// instead of implying the input was not a number.
|
|
errRetriesTooLarge = errors.New("retries out of range")
|
|
)
|
|
|
|
// parseMaxRetries interprets a max_retries form value.
|
|
//
|
|
// An ABSENT value — the field empty or not submitted — yields
|
|
// fallback, which lets the create path apply its default and the edit
|
|
// path leave the stored value alone. A value that is SET BUT INVALID
|
|
// is an error: unparseable, negative, or above maxTargetRetries.
|
|
//
|
|
// The distinction is the whole point of this function. max_retries=0
|
|
// means fire-and-forget, so returning 0 for input the operator typed
|
|
// but that did not parse silently disables retries on a
|
|
// store-and-forward proxy — and on the edit path it destroys a
|
|
// working retry configuration over a typo. A default answers a
|
|
// question that was not asked; it never answers one that was asked
|
|
// badly.
|
|
//
|
|
// A target stored with a count above the ceiling before this
|
|
// validation existed keeps rendering and keeps delivering — nothing
|
|
// clamps the row. Re-saving it from the edit form does have to bring
|
|
// it into range, because the form submits the pre-filled value back
|
|
// and accepting it would be the ceiling not applying to the edit
|
|
// path. The 400 names the ceiling, so the fix is one field.
|
|
func parseMaxRetries(raw string, fallback int) (int, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return fallback, nil
|
|
}
|
|
|
|
v, err := strconv.Atoi(raw)
|
|
if err != nil || v < 0 {
|
|
return 0, errRetriesInvalid
|
|
}
|
|
|
|
if v > maxTargetRetries {
|
|
return 0, errRetriesTooLarge
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// retriesErrorMessage returns the message the create and edit forms
|
|
// show for a rejected max_retries value. Any error other than
|
|
// errRetriesTooLarge falls back to the generic wording, so an
|
|
// unrecognised parse failure still produces a sensible 400.
|
|
func retriesErrorMessage(err error) string {
|
|
if errors.Is(err, errRetriesTooLarge) {
|
|
return errRetriesTooLarge.Error() +
|
|
": at most " + strconv.Itoa(maxTargetRetries) +
|
|
" retries"
|
|
}
|
|
|
|
return errRetriesInvalid.Error() +
|
|
", or 0 for fire-and-forget"
|
|
}
|