package handlers import ( "errors" "strconv" "strings" ) // maxTargetAttempts bounds a target's max_attempts. // // 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 maxTargetAttempts = 20 // Errors returned when a max_attempts form value cannot be turned into // a number of attempts. var ( // errAttemptsInvalid signals a max_attempts form value that is not // a whole number of at least 1. errAttemptsInvalid = errors.New( "must be a whole number, at least 1", ) // errAttemptsTooLarge signals a max_attempts form value that is a // whole number but above maxTargetAttempts. It is distinguished // from errAttemptsInvalid so the message can name the ceiling // instead of implying the input was not a number. errAttemptsTooLarge = errors.New("out of range") ) // parseMaxAttempts interprets a max_attempts 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, below 1, or above maxTargetAttempts. // // The distinction is the whole point of this function. Returning a // default for input the operator typed but that did not parse would // silently turn retries off on a store-and-forward proxy — and on the // edit path destroy a working configuration over a typo. A default // answers a question that was not asked; it never answers one that // was asked badly. func parseMaxAttempts(raw string, fallback int) (int, error) { raw = strings.TrimSpace(raw) if raw == "" { return fallback, nil } v, err := strconv.Atoi(raw) if err != nil || v < 1 { return 0, errAttemptsInvalid } if v > maxTargetAttempts { return 0, errAttemptsTooLarge } return v, nil } // attemptsErrorMessage returns the message the create and edit forms // show for a rejected max_attempts value. Any error other than // errAttemptsTooLarge falls back to the generic wording, so an // unrecognised parse failure still produces a sensible 400. func attemptsErrorMessage(err error) string { if errors.Is(err, errAttemptsTooLarge) { return errAttemptsTooLarge.Error() + ": at most " + strconv.Itoa(maxTargetAttempts) + " attempts" } return errAttemptsInvalid.Error() }