144 lines
5.0 KiB
Go
144 lines
5.0 KiB
Go
package httpx
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
"git.yuntuo.hk/k2safe/go-omni/response"
|
|
)
|
|
|
|
// SetResponseEncoder overrides this request's success AND error encoding. A
|
|
// cloned context inherits it, including buffered/idempotent requests.
|
|
func (c *Context) SetResponseEncoder(encoder response.Encoder) { c.encoder = encoder }
|
|
|
|
// WithResponse applies a policy to one route/group. It deliberately stays set
|
|
// until the outer error boundary finishes handling this request.
|
|
func WithResponse(encoder response.Encoder) Middleware {
|
|
return func(next Handler) Handler {
|
|
return func(c *Context) error { c.SetResponseEncoder(encoder); return next(c) }
|
|
}
|
|
}
|
|
|
|
// ResponseRegistrar installs the response policy before application middleware,
|
|
// so early auth/protection failures and buffered clones use the route contract.
|
|
type ResponseRegistrar interface {
|
|
HandleWithResponse(string, string, response.Encoder, Handler, ...Middleware)
|
|
}
|
|
|
|
// HandleWithResponse uses the registrar's early policy hook when available.
|
|
// Custom registrars without this hook get an ordinary leading route middleware.
|
|
func HandleWithResponse(router interface {
|
|
Handle(string, string, Handler, ...Middleware)
|
|
}, method, path string, encoder response.Encoder, handler Handler, middleware ...Middleware) {
|
|
if r, ok := router.(ResponseRegistrar); ok {
|
|
r.HandleWithResponse(method, path, encoder, handler, middleware...)
|
|
return
|
|
}
|
|
all := append([]Middleware{WithResponse(encoder)}, middleware...)
|
|
router.Handle(method, path, handler, all...)
|
|
}
|
|
|
|
func (c *Context) writeResult(result response.Result) (err error) {
|
|
if c.responded {
|
|
return nil
|
|
}
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
err = apierrors.Internal(fmt.Errorf("response encoder panic: %v", recovered))
|
|
}
|
|
}()
|
|
encoder := c.encoder
|
|
if encoder == nil {
|
|
encoder = response.Default()
|
|
}
|
|
if c.errorLocalizer != nil && result.Err != nil {
|
|
if message, ok := c.localizedMessage(result.Err); ok {
|
|
result.MessageOverride = &message
|
|
if c.failure != nil {
|
|
c.failure = c.failure.Clone()
|
|
c.failure.Message = message
|
|
}
|
|
}
|
|
}
|
|
payload, err := encoder.Encode(result)
|
|
if err != nil {
|
|
return apierrors.Internal(err)
|
|
}
|
|
if result.PreserveStatus {
|
|
payload.Status = result.Status
|
|
}
|
|
if payload.Status < 200 || payload.Status > 599 {
|
|
return apierrors.Internal(fmt.Errorf("response encoder returned invalid status %d", payload.Status))
|
|
}
|
|
if (payload.Status == 204 || payload.Status == 304) && len(payload.Body) > 0 {
|
|
return apierrors.Internal(fmt.Errorf("response encoder returned a body for status %d", payload.Status))
|
|
}
|
|
return c.WriteResponse(payload.Status, payload.Header, payload.Body)
|
|
}
|
|
|
|
// Presentation adapter failures cannot turn a business result into a new error.
|
|
func (c *Context) localizedMessage(err error) (message string, ok bool) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
message, ok = "", false
|
|
}
|
|
}()
|
|
registry := c.ErrorRegistry()
|
|
return c.errorLocalizer.Localize(c.Context(), registry, registry.Resolve(err))
|
|
}
|
|
|
|
// RespondStatus uses the current response format and error catalog, but keeps
|
|
// the explicit HTTP status even if a catalog/custom encoder selects another.
|
|
// This is useful for protocol endpoints such as health probes. Ordinary
|
|
// business handlers should use OK/Created or return their error as usual.
|
|
func (c *Context) RespondStatus(status int, data any, err error) error {
|
|
if c.Responded() {
|
|
return nil
|
|
}
|
|
if status < 200 || status > 599 || status == 204 || status == 205 || status == 304 {
|
|
return apierrors.Internal(fmt.Errorf("response requires a body-bearing HTTP status"))
|
|
}
|
|
result := response.Result{Status: status, Code: "OK", Message: "success", Data: data,
|
|
RequestID: c.RequestID(), PreserveStatus: true}
|
|
if err != nil {
|
|
failure := c.ResolveError(err)
|
|
failure.Status = status
|
|
c.failure = failure
|
|
result.Code, result.Message, result.BusinessCode = failure.Code, failure.Message, failure.BusinessCode
|
|
result.Err, result.Failed = err, true
|
|
}
|
|
writeErr := c.writeResult(result)
|
|
if writeErr != nil && !c.Responded() {
|
|
// A broken encoder must not fall through to a business error policy
|
|
// that acknowledges failures with HTTP 200. Commit a bodyless failure.
|
|
failureStatus := status
|
|
if failureStatus < 400 {
|
|
failureStatus = http.StatusInternalServerError
|
|
}
|
|
c.failure = c.ResolveError(writeErr)
|
|
c.failure.Status = failureStatus
|
|
_ = c.WriteResponse(failureStatus, nil, nil)
|
|
}
|
|
return writeErr
|
|
}
|
|
|
|
// Bytes, Text and XML are explicit wire responses; they bypass any envelope.
|
|
func (c *Context) Bytes(status int, contentType string, body []byte) error {
|
|
return c.WriteResponse(status, http.Header{"Content-Type": {contentType}}, body)
|
|
}
|
|
func (c *Context) Text(status int, body string) error {
|
|
return c.Bytes(status, "text/plain; charset=utf-8", []byte(body))
|
|
}
|
|
func (c *Context) XML(status int, value any) error {
|
|
if c.responded {
|
|
return nil
|
|
}
|
|
body, err := xml.Marshal(value)
|
|
if err != nil {
|
|
return apierrors.Internal(err)
|
|
}
|
|
return c.Bytes(status, "application/xml; charset=utf-8", body)
|
|
}
|