Fail deliveries on archive errors; validate expiry at creation (#43)
Some checks failed
check / check (push) Failing after 57s

Two review findings on the database archiving target:

- An archive error now records the attempt as failed with the
  error string and marks the delivery failed, instead of logging
  the error and reporting success. A target that could not do its
  one job must not claim it did.
- The archive expiry is now actually configurable: the add-target
  form gains an expiry field for database targets, and the value
  is validated at creation time via the new
  delivery.ValidateArchiveExpiry (empty, "never", or a positive
  Go duration), rejecting bad values with a 400 at the only place
  a human can fix them, mirroring how Slack target URLs are
  validated at creation.

Test updates: a forced archive failure asserts a failed delivery
with a recorded error and no archive file; config builder tests
cover empty/never/duration and rejection paths; the two engine
tests that exercise the database target now build engines with a
real webhook DB manager since archiving is no longer a no-op; the
reopen-debounce test uses a wider window so parallel test load
cannot make two rapid writes straddle it.
This commit is contained in:
2026-08-07 16:36:43 +00:00
parent 38cfe76d49
commit d35ad0c49e
8 changed files with 345 additions and 47 deletions

View File

@@ -5,6 +5,7 @@ import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi"
"github.com/google/uuid"
@@ -903,7 +904,9 @@ func (h *Handlers) buildTargetConfig(
return h.buildHTTPTargetConfig(w, r, targetURL)
case database.TargetTypeSlack:
return h.buildSlackTargetConfig(w, r, targetURL)
case database.TargetTypeDatabase, database.TargetTypeLog:
case database.TargetTypeDatabase:
return h.buildDatabaseTargetConfig(w, r)
case database.TargetTypeLog:
return "", nil
default:
http.Error(
@@ -1013,6 +1016,47 @@ func (h *Handlers) buildSlackTargetConfig(
return string(configBytes), nil
}
// buildDatabaseTargetConfig builds config JSON for a database
// (archive) target. The optional expiry form value is validated
// here, at creation time, so an unparseable value is rejected
// with a 400 instead of failing every subsequent delivery. An
// empty expiry yields an empty config (the keep-forever
// default).
func (h *Handlers) buildDatabaseTargetConfig(
w http.ResponseWriter,
r *http.Request,
) (string, error) {
expiry := strings.TrimSpace(r.FormValue("expiry"))
if expiry == "" {
return "", nil
}
err := delivery.ValidateArchiveExpiry(expiry)
if err != nil {
http.Error(
w,
"Invalid archive expiry: "+err.Error(),
http.StatusBadRequest,
)
return "", err
}
cfg := map[string]any{"expiry": expiry}
configBytes, err := json.Marshal(cfg)
if err != nil {
http.Error(
w, "Internal server error",
http.StatusInternalServerError,
)
return "", err
}
return string(configBytes), nil
}
// HandleEntrypointDelete handles deleting an entrypoint.
func (h *Handlers) HandleEntrypointDelete() http.HandlerFunc {
return h.deleteChildResource(