From d8eea66c4cad61e0d6fc647b4487e6e3b918eda7 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 2 Oct 2026 16:39:10 +0200 Subject: [PATCH] feat(errkind): Of -- a kind from a fixed vocabulary, never the error's text (review F3, F5) Co-Authored-By: Claude Sonnet 5.5 --- internal/errkind/errkind.go | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 internal/errkind/errkind.go diff --git a/internal/errkind/errkind.go b/internal/errkind/errkind.go new file mode 100644 index 0000000..74e9099 --- /dev/null +++ b/internal/errkind/errkind.go @@ -0,0 +1,42 @@ +// Package errkind reduces an error to a short kind that is safe to log, store and show. +// +// An error's own text can hold a provider's response body (a gateway that echoes the +// prompt), a file path (ffmpeg names the spool file), or a word from a private +// transcript. None of that may reach a log line, a database column or a page about a +// private recording (ADR-033: "typed, never a provider's string"; ADR-035). Of returns +// only a fixed vocabulary, so it can never carry any. +package errkind + +import ( + "context" + "errors" + "fmt" + "net" +) + +// HTTPStatuser is implemented by errors that are a non-2xx answer from a service. +type HTTPStatuser interface{ HTTPStatus() int } + +// Of is the kind of err: "timeout", "canceled", "http_", "network", or "error". +// It is empty for nil. +func Of(err error) string { + if err == nil { + return "" + } + var hs HTTPStatuser + var ne net.Error + switch { + case errors.Is(err, context.DeadlineExceeded): + return "timeout" + case errors.Is(err, context.Canceled): + return "canceled" + case errors.As(err, &hs): + return fmt.Sprintf("http_%d", hs.HTTPStatus()) + case errors.As(err, &ne): + if ne.Timeout() { + return "timeout" + } + return "network" + } + return "error" +}