Files
go-omni/httpx/response.go

124 lines
4.4 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()
}
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)
}
// 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)
}