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() }