response.go
Rama master · réplica de lectura del repositorio original.
package aastro
import (
"net/http"
"github.com/goccy/go-json"
)
// ProblemDetails is the RFC 9457 ("Problem Details for HTTP APIs") body used
// for every gateway-authored response that carries no upstream data of its
// own - rate limiting, payload limits, plugin failures, and upstream/gateway
// failures that never produced anything worth returning as data.
//
// A response that *does* carry data (a full or partial aggregation success)
// is never wrapped: the body is the aggregated payload itself, exactly as a
// client of the upstream(s) directly would see it. See Router.buildResponse.
type ProblemDetails struct {
// Type is always "about:blank" (RFC 9457 §4.2's own default for "no
// further-specific type"): it is never a dereferencable URI on purpose.
// A real one - even a stable, non-existent one under the gateway's own
// docs domain - names the software fronting this upstreams to anyone
// who receives an error, which is itself reconnaissance: it tells a
// caller they're behind an aggregating gateway and invites probing for
// what that implies about the backend topology. Errors carries the
// actual discriminator instead - a closed, generic enum that says
// nothing about what's behind the gateway.
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
// Errors lists every distinct underlying ClientError - always at least
// one. This is the one machine-readable discriminator (Type is constant,
// see above); Title is for humans and may change wording over time.
// More than one entry only for a multi-upstream flow where several
// upstreams failed differently. This is a problem type extension member
// (RFC 9457 §3.2), not a spec violation.
Errors []ClientError `json:"errors"`
}
// problemTypeBlank is RFC 9457's own placeholder for "the HTTP status code
// is all the classification there is".
const problemTypeBlank = "about:blank"
type ClientError string
const (
ClientErrRateLimitExceeded ClientError = "RATE_LIMIT_EXCEEDED"
ClientErrPayloadTooLarge ClientError = "PAYLOAD_TOO_LARGE"
ClientErrUpstreamBodyTooLarge ClientError = "UPSTREAM_BODY_TOO_LARGE"
ClientErrUpstreamUnavailable ClientError = "UPSTREAM_UNAVAILABLE"
ClientErrUpstreamError ClientError = "UPSTREAM_ERROR"
ClientErrUpstreamClientError ClientError = "UPSTREAM_CLIENT_ERROR"
ClientErrUpstreamRedirect ClientError = "UPSTREAM_REDIRECT"
ClientErrUpstreamMalformed ClientError = "UPSTREAM_MALFORMED"
ClientErrInternal ClientError = "INTERNAL"
ClientErrAborted ClientError = "ABORTED"
ClientErrValueConflict ClientError = "VALUE_CONFLICT"
ClientErrUnauthorized ClientError = "UNAUTHORIZED"
)
func (err ClientError) String() string {
return string(err)
}
// problemTitle returns the RFC 9457 Title for this error code - a short,
// human-readable summary that does not change from one occurrence to the
// next (request-specific detail, when there is any, belongs in Detail).
func (err ClientError) problemTitle() string {
if title, ok := problemTitles[err]; ok {
return title
}
return string(err)
}
var problemTitles = map[ClientError]string{
ClientErrRateLimitExceeded: "Rate limit exceeded",
ClientErrPayloadTooLarge: "Request payload too large",
ClientErrUpstreamBodyTooLarge: "Upstream response too large",
ClientErrUpstreamUnavailable: "Upstream unavailable",
ClientErrUpstreamError: "Upstream returned a server error",
ClientErrUpstreamClientError: "Upstream rejected the request",
ClientErrUpstreamRedirect: "Upstream returned a redirect",
ClientErrUpstreamMalformed: "Upstream response failed gateway policy",
ClientErrInternal: "Internal gateway error",
ClientErrAborted: "Request aborted before the upstream responded",
ClientErrValueConflict: "Conflicting values from upstreams",
ClientErrUnauthorized: "Unauthorized",
}
// WriteError writes a single-cause RFC 9457 Problem Details response:
// rate limiting, request-size limits, plugin failures, and any other
// gateway-side rejection that never reaches an upstream at all.
func WriteError(w http.ResponseWriter, code ClientError, status int) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(status)
problem := ProblemDetails{
Type: problemTypeBlank,
Title: code.problemTitle(),
Status: status,
Errors: []ClientError{code},
}
if err := json.NewEncoder(w).Encode(problem); err != nil {
// Fallback on error
http.Error(w, http.StatusText(status), status)
}
}