a Hexis 401 says the token was refused, not that Hexis is down (V-587)

The vendored Hexis client is a separate implementation and returns a plain
fmt.Errorf for every status at or above 400, so errors.As for *ecosystemError
never matched, Unauthorized() was never consulted, and ecosystemGap always fell
through to the outage line. A wrong token sent him to inspect a healthy service.

hexisError classifies at Maven's boundary, since the client is vendored from
another repo and a local edit there is lost on the next re-vendor. The status
text is the only signal that survives the wrapping, so that is what it reads;
anything unrecognised stays at status 0, which is what Unreachable() means. The
correct fix is a typed error upstream carrying the code, and Maven cannot land
it unilaterally.

execHexis is the second site and it did not call ecosystemGap at all. It now
does, but only for a failure that belongs to the service. An execution that Hexis
accepted and that then failed keeps the command-level line: that is the command
failing, not Hexis degrading, and calling it an outage would be the same defect
pointed the other way. Authorization is unchanged: a 401 is still a refusal, it
is not retried and nothing proceeds on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 02:48:00 +04:00
parent d5d4166710
commit 5d2fd91c06
3 changed files with 229 additions and 1 deletions
+54 -1
View File
@@ -12,6 +12,7 @@ import (
"log"
"net/http"
"net/url"
"strings"
"time"
hexisclient "github.com/kami/hexis/pkg/client"
@@ -179,6 +180,54 @@ func httpError(service, op string, status int) *ecosystemError {
}
}
// hexisStatusTexts maps the http.StatusText spelling back to its code, for the
// failure statuses a Hexis call can plausibly answer with. It is the inverse of
// what the vendored client threw away.
var hexisStatusTexts = func() map[string]int {
codes := []int{
http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden,
http.StatusNotFound, http.StatusMethodNotAllowed, http.StatusNotAcceptable,
http.StatusRequestTimeout, http.StatusConflict, http.StatusGone,
http.StatusUnprocessableEntity, http.StatusUpgradeRequired,
http.StatusTooManyRequests, http.StatusInternalServerError,
http.StatusNotImplemented, http.StatusBadGateway,
http.StatusServiceUnavailable, http.StatusGatewayTimeout,
}
m := make(map[string]int, len(codes))
for _, c := range codes {
m[http.StatusText(c)] = c
}
return m
}()
// hexisError re-wraps an error from the vendored Hexis client as an
// *ecosystemError, so a Hexis failure classifies the same way a Nexus or Praxis
// one does and ecosystemGap can tell a refused credential from an outage.
//
// This is a boundary adapter and it is not the fix anyone would choose. The
// Hexis client lives in another repository and returns
// fmt.Errorf("%s: %s", http.StatusText(status), body) for every status at or
// above 400, so the status text is the only signal that survives — the correct
// fix is a typed error carrying the code, and Maven cannot land it unilaterally
// (Vikunja #587, docs/plans/20-two-artifacts-and-neither-is-spring.md). Parsing
// here is bounded: the message's first colon-delimited segment is the status
// text verbatim, no status text contains a colon, and anything unrecognised —
// "do request: ...", "create request: ..." — is a transport failure and is left
// at status 0, which is exactly what Unreachable() means.
func hexisError(op string, err error) error {
if err == nil {
return nil
}
var ee *ecosystemError
if errors.As(err, &ee) {
return err
}
head, _, _ := strings.Cut(err.Error(), ": ")
return &ecosystemError{
Service: "hexis", Op: op, Status: hexisStatusTexts[head], Err: err,
}
}
type nexusClient struct {
ecosystemHTTP
}
@@ -530,6 +579,7 @@ func (w *ecosystemWiring) discoverCapabilities(ctx context.Context, entityID str
}
caps, err := w.hexis.Capabilities(ctx, entityID)
if err != nil {
err = hexisError("capabilities", err)
log.Printf("ecosystem: hexis capabilities error: %v", err)
return nil, err
}
@@ -557,7 +607,10 @@ func (w *ecosystemWiring) executeCapability(ctx context.Context, capabilityID, t
exec, err := w.hexis.Execute(ctx, req)
if err != nil {
return correlationID, fmt.Errorf("execute: %w", err)
// A classified dependency failure. The two returns below are NOT: an
// execution that ran and failed is the command failing, not Hexis
// degrading, and it keeps its plain error so the caller says so.
return correlationID, hexisError("execute", err)
}
if exec.Status == "succeeded" {
return correlationID, nil