Files
go-omni/httpx/context.go

328 lines
9.8 KiB
Go

package httpx
import (
"context"
"encoding/json"
"errors"
"net/http"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/logx"
"git.yuntuo.hk/k2safe/go-omni/response"
"git.yuntuo.hk/k2safe/go-omni/validate"
)
// Handler is the function signature used by GoOmni routes.
type Handler func(*Context) error
// Middleware wraps a Handler and may enforce cross-cutting behavior.
type Middleware func(Handler) Handler
// Router is the small contract the application needs from a network layer.
// The default implementation is backed by the standard library, but another
// router or a future GoOmni transport can implement this interface without
// changing business handlers. Register routes before serving requests. Implementations
// should pass transport/transporttest.Run; see transport/README.md for the
// portable path, method, fallback and context-propagation rules.
type Router interface {
http.Handler
Handle(method, path string, handler http.Handler)
Any(path string, handler http.Handler)
Group(path string) RouterGroup
}
// FallbackRouter optionally routes 404/405 through the application's response
// policy and middleware. Custom adapters can implement it without changing Router.
type FallbackRouter interface {
SetFallbacks(notFound, methodNotAllowed http.Handler)
}
// RouterGroup is the route-registration portion of Router.
type RouterGroup interface {
Handle(method, path string, handler http.Handler)
Any(path string, handler http.Handler)
Group(path string) RouterGroup
}
type requestContextKey uint8
const (
routeParamsKey requestContextKey = iota + 1
routePatternKey
)
// WithRouteParams lets a router adapter expose path parameters to the
// transport-neutral Context.
func WithRouteParams(ctx context.Context, params map[string]string) context.Context {
copyOfParams := make(map[string]string, len(params))
for key, value := range params {
copyOfParams[key] = value
}
return context.WithValue(ctx, routeParamsKey, copyOfParams)
}
// RouteParams returns path parameters installed by a router adapter.
func RouteParams(ctx context.Context) map[string]string {
if ctx == nil {
return nil
}
params, _ := ctx.Value(routeParamsKey).(map[string]string)
return params
}
// WithRoutePattern records the router's matched route pattern for access logs.
func WithRoutePattern(ctx context.Context, pattern string) context.Context {
return context.WithValue(ctx, routePatternKey, pattern)
}
// RoutePattern returns the matched route pattern, when the adapter provides it.
func RoutePattern(ctx context.Context) string {
if ctx == nil {
return ""
}
pattern, _ := ctx.Value(routePatternKey).(string)
return pattern
}
// Context provides the business-facing request and response API. It contains
// only standard-library HTTP types, so it remains usable when the network
// implementation is replaced.
type Context struct {
request *http.Request
writer http.ResponseWriter
params map[string]string
values map[string]any
responded bool
status int
encoder response.Encoder
errorRegistry *apierrors.Registry
failure *apierrors.APIError
}
// NewContext creates a transport-neutral GoOmni context.
func NewContext(request *http.Request, writer http.ResponseWriter, params map[string]string) *Context {
if request == nil {
request = &http.Request{}
}
c := &Context{
request: request,
params: params,
values: make(map[string]any),
}
if writer != nil {
c.writer = trackWriter(writer, c)
}
return c
}
func (c *Context) Request() *http.Request { return c.request }
// Context returns the standard request context. It carries GoOmni
// correlation values such as the request ID and can be passed to services.
func (c *Context) Context() context.Context {
if c.request == nil {
return context.Background()
}
return c.request.Context()
}
// Raw returns a streaming response writer that tracks commits and status.
// Optional HTTP capabilities are preserved only when the underlying writer supports them.
func (c *Context) Raw() http.ResponseWriter { return c.writer }
// Clone gives a buffering middleware its own response state and values map.
// Stored values themselves are shared; middleware must not mutate them concurrently.
func (c *Context) Clone(ctx context.Context, writer http.ResponseWriter) *Context {
clone := NewContext(c.request.Clone(ctx), writer, c.params)
clone.encoder = c.encoder
clone.errorRegistry = c.errorRegistry
for key, value := range c.values {
clone.values[key] = value
}
return clone
}
// WriteResponse commits a buffered response through GoOmni's response tracking.
func (c *Context) WriteResponse(status int, headers http.Header, body []byte) error {
if c.responded {
return nil
}
if c.writer == nil {
return apierrors.Internal(nil)
}
for key, values := range headers {
c.writer.Header()[key] = append([]string(nil), values...)
}
c.writer.WriteHeader(status)
if len(body) > 0 {
if _, err := c.writer.Write(body); err != nil {
return apierrors.Internal(err)
}
}
return nil
}
func (c *Context) Param(name string) string { return c.params[name] }
func (c *Context) Query(name string) string {
if c.request == nil {
return ""
}
return c.request.URL.Query().Get(name)
}
func (c *Context) Header(name string) string {
if c.request == nil {
return ""
}
return c.request.Header.Get(name)
}
func (c *Context) RequestID() string { return logx.RequestID(c.Context()) }
func (c *Context) Set(key string, value any) { c.values[key] = value }
func (c *Context) Get(key string) (any, bool) {
value, ok := c.values[key]
return value, ok
}
// WithContext replaces the standard request context while preserving the
// request and its headers.
func (c *Context) WithContext(ctx context.Context) {
if c.request != nil {
c.request = c.request.WithContext(ctx)
}
}
// SetResponseHeader sets a header on the underlying response.
func (c *Context) SetResponseHeader(name, value string) {
if c.writer != nil {
c.writer.Header().Set(name, value)
}
}
func (c *Context) BindJSON(value any) error { return validate.JSON(c.request, value) }
func (c *Context) BindQuery(value any) error { return validate.Query(c.request, value) }
func (c *Context) BindURI(value any) error { return validate.URI(c.params, value) }
// JSON is an escape hatch for responses that do not use the standard envelope.
func (c *Context) JSON(status int, value any) error {
if c.responded {
return nil
}
if c.writer == nil {
return apierrors.Internal(nil)
}
// Encode before committing headers so an invalid value can still produce
// the standard 500 response on every adapter.
data, err := json.Marshal(value)
if err != nil {
return apierrors.Internal(err)
}
c.writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.writer.WriteHeader(status)
if _, err := c.writer.Write(append(data, '\n')); err != nil {
return apierrors.Internal(err)
}
return nil
}
func (c *Context) OK(data any) error { return c.respond(http.StatusOK, "OK", "success", data) }
func (c *Context) Created(data any) error {
return c.respond(http.StatusCreated, "CREATED", "created", data)
}
func (c *Context) Accepted(data any) error {
return c.respond(http.StatusAccepted, "ACCEPTED", "accepted", data)
}
func (c *Context) NoContent() error {
if c.responded {
return nil
}
if c.writer == nil {
return apierrors.Internal(nil)
}
c.writer.WriteHeader(http.StatusNoContent)
return nil
}
func (c *Context) respond(status int, code, message string, data any) error {
return c.writeResult(response.Result{
Status: status,
Code: code,
Message: message,
Data: data,
RequestID: c.RequestID(),
})
}
// Status returns the response status observed by Context.
func (c *Context) Status() int {
if c.status == 0 {
return http.StatusOK
}
return c.status
}
// Responded reports whether this handler has already written a response.
func (c *Context) Responded() bool { return c.responded }
// WriteError writes a stable public error response. Internal causes are not
// serialized. Responses committed through Context methods or Raw are preserved.
func (c *Context) WriteError(err error) {
if c.responded || err == nil {
return
}
var bodyErr *http.MaxBytesError
if errors.As(err, &bodyErr) {
err = bodyTooLarge()
}
apiErr := c.ResolveError(err)
c.failure = apiErr
if writeErr := c.writeResult(response.Result{
BusinessCode: apiErr.BusinessCode,
Failed: true,
Status: apiErr.Status,
Code: apiErr.Code,
Message: apiErr.Message,
RequestID: c.RequestID(),
Err: err,
}); writeErr != nil && !c.responded {
// An invalid custom encoder must not recurse or leak a different envelope.
_ = c.WriteResponse(http.StatusInternalServerError, nil, nil)
}
}
// SetErrorRegistry sets an application fallback. Route encoders can supply their
// own registry; clones retain both policies without any process-global lookup.
func (c *Context) SetErrorRegistry(r *apierrors.Registry) { c.errorRegistry = r }
func (c *Context) ResolveError(err error) *apierrors.APIError {
registry := c.errorRegistry
if provider, ok := c.encoder.(response.ErrorRegistryProvider); ok {
if scoped := provider.ErrorRegistry(); scoped != nil {
registry = scoped
}
}
return registry.Resolve(err)
}
// RecordError preserves failure semantics when a middleware commits buffered bytes.
func (c *Context) RecordError(err error) { c.failure = c.ResolveError(err) }
// RecordFailure propagates an already resolved failure without reapplying a
// different parent/catalog policy. Used with committed or buffered responses.
func (c *Context) RecordFailure(err *apierrors.APIError) { c.failure = err.Clone() }
// Failure returns a detached description of a failure already handled by Context.
func (c *Context) Failure() *apierrors.APIError {
if c.failure == nil {
return nil
}
return c.failure.Clone()
}