fluid-core/internal/runtime/response.go
tegwick 791e419973
Some checks failed
ci / build (push) Failing after 23s
Add connector, response policy, telemetry emitter and gateway
Completes the request path for FLUID-WP-0003 T01-T03 and T05-T07. The
gateway resolves a revision, routes to the adapter process, and records
what happened, without ever depending on the control plane to serve.

Three behaviours carry tests because the architecture rests on them:
Emit never blocks against a stalled sink (Blueprint 34.2), the gateway
keeps serving after control-plane loss (invariant 2), and backend
internals do not leak into error responses (5.7).

The connector retries only idempotent methods, so a slow adapter cannot
cause a hall-of-helix entry to be published twice, and breakers are
per-revision so a broken candidate does not take the stable revision
down with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1116572@bnt-lap001
Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
2026-09-04 02:09:49 +02:00

91 lines
3.2 KiB
Go

package runtime
import (
"encoding/json"
"net/http"
"github.com/tegwick/fluid-core/internal/contract"
)
// ErrorKind classifies a response the interface produced rather than the
// backend. These map onto the telemetry error classes so that a response and
// the evidence it generates cannot disagree.
type ErrorKind string
const (
ErrorValidation ErrorKind = "validation"
ErrorUnknownPath ErrorKind = "unknown_path"
ErrorUnsupported ErrorKind = "unsupported_parameter"
ErrorAuthorization ErrorKind = "authorization"
ErrorUnavailable ErrorKind = "backend_failure"
ErrorTimeout ErrorKind = "timeout"
ErrorPolicy ErrorKind = "policy_rejection"
ErrorNoCapability ErrorKind = "missing_capability"
)
// ErrorBody is the interface's error representation.
//
// ArchitectureBlueprint.md section 5.7: errors should carry enough structured
// information to improve observability without leaking backend detail, and may
// include a correlation reference that lets downstream analysis link the
// response a consumer saw to the pressure it generated.
type ErrorBody struct {
Kind ErrorKind `json:"kind"`
// Message is written for the consumer and must stay free of backend
// internals: hostnames, stack traces, driver errors, upstream payloads.
Message string `json:"message"`
// Correlation is the reference a consumer can quote back, and the key that
// ties this response to its telemetry.
Correlation string `json:"correlation"`
// Revision tells the consumer which contract answered. Without it a client
// debugging an unexpected response has no way to know what it was talking to.
Revision contract.RevisionID `json:"revision,omitempty"`
// Field names the offending input for validation failures.
Field string `json:"field,omitempty"`
// Feedback points at the explicit-feedback endpoint. Turning a dead end into
// an invitation is the cheapest pressure signal the interface can collect.
Feedback string `json:"feedback,omitempty"`
}
// statusFor maps an error kind to its HTTP status.
var statusFor = map[ErrorKind]int{
ErrorValidation: http.StatusBadRequest,
ErrorUnknownPath: http.StatusNotFound,
ErrorUnsupported: http.StatusBadRequest,
ErrorAuthorization: http.StatusForbidden,
ErrorUnavailable: http.StatusBadGateway,
ErrorTimeout: http.StatusGatewayTimeout,
ErrorPolicy: http.StatusForbidden,
ErrorNoCapability: http.StatusNotImplemented,
}
// ResponsePolicy renders interface errors consistently.
type ResponsePolicy struct {
// FeedbackPath, when set, is advertised on every error.
FeedbackPath string
}
// WriteError renders an error response.
func (p ResponsePolicy) WriteError(w http.ResponseWriter, kind ErrorKind, correlation, message string, rev contract.RevisionID, field string) {
status, ok := statusFor[kind]
if !ok {
status = http.StatusInternalServerError
}
body := ErrorBody{
Kind: kind,
Message: message,
Correlation: correlation,
Revision: rev,
Field: field,
Feedback: p.FeedbackPath,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-FLUID-Correlation", correlation)
if rev != "" {
w.Header().Set("X-FLUID-Revision", string(rev))
}
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}