Files
nmon/internal/notify/webhook.go
jleung 287fbb98bd Maintainability cleanup (core libraries): 223 lint findings to 0, oversized files split; fix apc-ups label columns and never poll text metrics
Migration files restructured with shared helpers; verified byte-identical SQLite schema vs previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 19:11:15 +00:00

49 lines
1.5 KiB
Go

package notify
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/v10networks/nmon/internal/store"
)
// HeaderIncident carries the incident ID on every webhook request.
const HeaderIncident = "X-Nmon-Incident"
// sendWebhook POSTs the stored (already redacted) payload.
//
// Outcome: 2xx is success. 408, 429 and 5xx are retried, as are network
// errors and timeouts. Every other status (4xx, and unfollowed 3xx) is a
// permanent failure: the receiver understood the request and refused it, so
// resending the same payload will not help. A destination denied by the
// outbound policy is also permanent.
func (q *Queue) sendWebhook(ctx context.Context, c *compiled, d store.Delivery) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, strings.NewReader(d.Payload))
if err != nil {
return permanent{err}
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "nmon")
for k, v := range c.Headers {
req.Header.Set(k, v)
}
req.Header.Set(HeaderIncident, d.IncidentID) // not overridable by config
resp, err := q.http.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }() // body is drained below
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64<<10))
switch s := resp.StatusCode; {
case s >= 200 && s < 300:
return nil
case s == http.StatusRequestTimeout || s == http.StatusTooManyRequests || s >= 500:
return fmt.Errorf("webhook returned HTTP %d", s)
default:
return permanent{fmt.Errorf("webhook returned HTTP %d", s)}
}
}