120 lines
3.8 KiB
Go
120 lines
3.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"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"
|
|
}
|
|
|
|
// targetMaxRetries reads and validates max_retries from a target form
|
|
// submission, answering the request with a 400 and reporting false
|
|
// when the value is set but invalid.
|
|
//
|
|
// Both the create and the edit path go through here, so the two
|
|
// cannot come to disagree about what a valid retry count is. The
|
|
// wording matches the timeout control on the same submission.
|
|
func targetMaxRetries(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
fallback int,
|
|
) (int, bool) {
|
|
retries, err := parseMaxRetries(
|
|
r.PostFormValue("max_retries"), fallback,
|
|
)
|
|
if err != nil {
|
|
http.Error(
|
|
w,
|
|
"Invalid max retries: "+retriesErrorMessage(err),
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return 0, false
|
|
}
|
|
|
|
return retries, true
|
|
}
|