Files
2026-06-12 13:43:37 +02:00

821 lines
26 KiB
Go

// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
stdContext "context"
stderrors "errors"
"fmt"
"net/http"
"strings"
"sync"
"github.com/go-openapi/analysis"
"github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag/typeutils"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/logger"
"github.com/go-openapi/runtime/middleware/untyped"
"github.com/go-openapi/runtime/security"
"github.com/go-openapi/runtime/server-middleware/docui"
"github.com/go-openapi/runtime/server-middleware/mediatype"
"github.com/go-openapi/runtime/server-middleware/negotiate"
)
// Debug when true turns on verbose logging.
var Debug = logger.DebugEnabled()
// Logger is the standard library logger used for printing debug messages.
var Logger logger.Logger = logger.StandardLogger{}
func debugLogfFunc(lg logger.Logger) func(string, ...any) {
if logger.DebugEnabled() {
if lg == nil {
return Logger.Debugf
}
return lg.Debugf
}
// muted logger
return func(_ string, _ ...any) {}
}
// A Builder can create middlewares.
type Builder func(http.Handler) http.Handler
// PassthroughBuilder returns the handler, aka the builder identity function.
func PassthroughBuilder(handler http.Handler) http.Handler { return handler }
// RequestBinder is an interface for types to implement
// when they want to be able to bind from a request.
type RequestBinder interface {
BindRequest(*http.Request, *MatchedRoute) error
}
// Responder is an interface for types to implement
// when they want to be considered for writing HTTP responses.
type Responder interface {
WriteResponse(http.ResponseWriter, runtime.Producer)
}
// ResponderFunc wraps a func as a Responder interface.
type ResponderFunc func(http.ResponseWriter, runtime.Producer)
// WriteResponse writes to the response.
func (fn ResponderFunc) WriteResponse(rw http.ResponseWriter, pr runtime.Producer) {
fn(rw, pr)
}
// Context is a type safe wrapper around an [untyped] request context
// used throughout to store request context with the standard context attached
// to the [http.Request].
type Context struct {
spec *loads.Document
analyzer *analysis.Spec
api RoutableAPI
router Router
debugLogf func(string, ...any) // a logging function to debug context and all components using it
ignoreParameters bool // see SetIgnoreParameters / WithIgnoreParameters
matchSuffix bool // see SetMatchSuffix / WithMatchSuffix
}
// NewRoutableContext creates a new context for a routable API.
//
// If a nil Router is provided, the [DefaultRouter] ([denco]-based) will be used.
func NewRoutableContext(spec *loads.Document, routableAPI RoutableAPI, routes Router) *Context {
var an *analysis.Spec
if spec != nil {
an = analysis.New(spec.Spec())
}
return NewRoutableContextWithAnalyzedSpec(spec, an, routableAPI, routes)
}
// NewRoutableContextWithAnalyzedSpec is like [NewRoutableContext] but takes as input an already analysed spec.
//
// If a nil Router is provided, the [DefaultRouter] ([denco]-based) will be used.
func NewRoutableContextWithAnalyzedSpec(spec *loads.Document, an *analysis.Spec, routableAPI RoutableAPI, routes Router) *Context {
// Either there are no spec doc and analysis, or both of them.
if (spec != nil || an != nil) && (spec == nil || an == nil) {
panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, "routable context requires either both spec doc and analysis, or none of them"))
}
return &Context{
spec: spec,
api: routableAPI,
analyzer: an,
router: routes,
debugLogf: debugLogfFunc(nil),
}
}
// NewContext creates a new context wrapper.
//
// If a nil Router is provided, the [DefaultRouter] ([denco]-based) will be used.
func NewContext(spec *loads.Document, api *untyped.API, routes Router) *Context {
var an *analysis.Spec
if spec != nil {
an = analysis.New(spec.Spec())
}
ctx := &Context{
spec: spec,
analyzer: an,
router: routes,
debugLogf: debugLogfFunc(nil),
}
ctx.api = newRoutableUntypedAPI(spec, api, ctx)
return ctx
}
// Serve serves the specified spec with the specified api registrations as a [http.Handler].
func Serve(spec *loads.Document, api *untyped.API) http.Handler {
return ServeWithBuilder(spec, api, PassthroughBuilder)
}
// SetIgnoreParameters toggles the legacy parameter-stripping behaviour for
// Accept negotiation server-wide. When set, every internal call to
// [NegotiateContentType] from this Context applies [WithIgnoreParameters].
//
// Returns the receiver for fluent configuration:
//
// ctx := middleware.NewContext(spec, api, nil).SetIgnoreParameters(true)
//
// See [WithIgnoreParameters] for the rationale and an example.
func (c *Context) SetIgnoreParameters(ignore bool) *Context {
c.ignoreParameters = ignore
return c
}
// SetMatchSuffix toggles RFC 6839 structured-syntax suffix tolerance
// server-wide. When enabled, both Accept negotiation and codec lookup
// fall back through the suffix base for the recognised suffixes
// (+json, +xml, +yaml) — so an operation declaring
// consumes: [application/json] also accepts request bodies sent with
// Content-Type: application/vnd.api+json (or any other +json variant).
//
// Default: strict (false). Use only when interoperating with clients
// that do not strictly abide by the spec.
//
// Returns the receiver for fluent configuration:
//
// ctx := middleware.NewContext(spec, api, nil).SetMatchSuffix(true)
//
// See [negotiate.WithMatchSuffix] for the per-call form and rationale.
func (c *Context) SetMatchSuffix(enable bool) *Context {
c.matchSuffix = enable
return c
}
type routableUntypedAPI struct {
api *untyped.API
hlock *sync.Mutex
handlers map[string]map[string]http.Handler
defaultConsumes string
defaultProduces string
}
func newRoutableUntypedAPI(spec *loads.Document, api *untyped.API, context *Context) *routableUntypedAPI {
var handlers map[string]map[string]http.Handler
if spec == nil || api == nil {
return nil
}
analyzer := analysis.New(spec.Spec())
for method, hls := range analyzer.Operations() {
um := strings.ToUpper(method)
for path, op := range hls {
schemes := analyzer.SecurityRequirementsFor(op)
oh, ok := api.OperationHandlerFor(method, path)
if !ok {
continue
}
if handlers == nil {
handlers = make(map[string]map[string]http.Handler)
}
if b, ok := handlers[um]; !ok || b == nil {
handlers[um] = make(map[string]http.Handler)
}
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// lookup route info in the context
route, rCtx, _ := context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
// bind and validate the request using reflection
var bound any
var validation error
bound, r, validation = context.BindAndValidate(r, route)
if validation != nil {
context.Respond(w, r, route.Produces, route, validation)
return
}
// actually handle the request
result, err := oh.Handle(bound)
if err != nil {
// respond with failure
context.Respond(w, r, route.Produces, route, err)
return
}
// respond with success
context.Respond(w, r, route.Produces, route, result)
})
if len(schemes) > 0 {
handler = newSecureAPI(context, handler)
}
handlers[um][path] = handler
}
}
return &routableUntypedAPI{
api: api,
hlock: new(sync.Mutex),
handlers: handlers,
defaultProduces: api.DefaultProduces,
defaultConsumes: api.DefaultConsumes,
}
}
func (r *routableUntypedAPI) HandlerFor(method, path string) (http.Handler, bool) {
r.hlock.Lock()
paths, ok := r.handlers[strings.ToUpper(method)]
if !ok {
r.hlock.Unlock()
return nil, false
}
handler, ok := paths[path]
r.hlock.Unlock()
return handler, ok
}
func (r *routableUntypedAPI) ServeErrorFor(_ string) func(http.ResponseWriter, *http.Request, error) {
return r.api.ServeError
}
func (r *routableUntypedAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer {
return r.api.ConsumersFor(mediaTypes)
}
func (r *routableUntypedAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer {
return r.api.ProducersFor(mediaTypes)
}
func (r *routableUntypedAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator {
return r.api.AuthenticatorsFor(schemes)
}
func (r *routableUntypedAPI) Authorizer() runtime.Authorizer {
return r.api.Authorizer()
}
func (r *routableUntypedAPI) Formats() strfmt.Registry {
return r.api.Formats()
}
func (r *routableUntypedAPI) DefaultProduces() string {
return r.defaultProduces
}
func (r *routableUntypedAPI) DefaultConsumes() string {
return r.defaultConsumes
}
// ServeWithBuilder serves the specified spec with the specified api registrations as a [http.Handler] that is decorated
// by the Builder.
func ServeWithBuilder(spec *loads.Document, api *untyped.API, builder Builder) http.Handler {
context := NewContext(spec, api, nil)
return context.APIHandler(builder)
}
type contextKey int8
const (
_ contextKey = iota
ctxContentType
ctxResponseFormat
ctxMatchedRoute
ctxBoundParams
ctxSecurityPrincipal
ctxSecurityScopes
)
// MatchedRouteFrom request context value.
func MatchedRouteFrom(req *http.Request) *MatchedRoute {
mr := req.Context().Value(ctxMatchedRoute)
if mr == nil {
return nil
}
if res, ok := mr.(*MatchedRoute); ok {
return res
}
return nil
}
// SecurityPrincipalFrom request context value.
func SecurityPrincipalFrom(req *http.Request) any {
return req.Context().Value(ctxSecurityPrincipal)
}
// SecurityScopesFrom request context value.
func SecurityScopesFrom(req *http.Request) []string {
rs := req.Context().Value(ctxSecurityScopes)
if res, ok := rs.([]string); ok {
return res
}
return nil
}
type contentTypeValue struct {
MediaType string
Charset string
}
// BasePath returns the base path for this API.
func (c *Context) BasePath() string {
if c.spec == nil {
return ""
}
return c.spec.BasePath()
}
// SetLogger allows for injecting a logger to catch debug entries.
//
// The logger is enabled in DEBUG mode only.
func (c *Context) SetLogger(lg logger.Logger) {
c.debugLogf = debugLogfFunc(lg)
}
// RequiredProduces returns the accepted content types for responses.
func (c *Context) RequiredProduces() []string {
return c.analyzer.RequiredProduces()
}
// BindValidRequest binds a params object to a request but only when the request is valid
// if the request is not valid an error will be returned.
func (c *Context) BindValidRequest(request *http.Request, route *MatchedRoute, binder RequestBinder) error {
var requestContentType string
// check and validate content type, select consumer
if runtime.HasBody(request) {
ct, cons, err := c.bindRequestBody(request, route)
if err != nil {
return errors.CompositeValidationError(err)
}
// happy path
requestContentType = ct
route.Consumer = cons
}
// check and validate the response format
// if the route does not provide Produces and a default contentType could not be identified
// based on a body, typical for GET and DELETE requests, then default contentType to.
if len(route.Produces) == 0 && requestContentType == "" {
requestContentType = "*/*"
}
str := negotiate.ContentType(request, route.Produces, requestContentType, c.negotiateOpts()...)
if str == "" {
return errors.CompositeValidationError(
errors.InvalidResponseFormat(request.Header.Get(runtime.HeaderAccept), route.Produces),
)
}
if binder == nil {
return nil
}
// now bind the request with the provided binder
// it's assumed the binder will also validate the request and return an error if the
// request is invalid
return binder.BindRequest(request, route)
}
// ContentType gets the parsed value of a content type
// Returns the media type, its charset and a shallow copy of the request
// when its context doesn't contain the content type value, otherwise it returns
// the same request
// Returns the error that [runtime.ContentType] may returns.
func (c *Context) ContentType(request *http.Request) (string, string, *http.Request, error) {
var rCtx = request.Context()
if v, ok := rCtx.Value(ctxContentType).(*contentTypeValue); ok {
return v.MediaType, v.Charset, request, nil
}
mt, cs, err := runtime.ContentType(request.Header)
if err != nil {
return "", "", nil, err
}
rCtx = stdContext.WithValue(rCtx, ctxContentType, &contentTypeValue{mt, cs})
return mt, cs, request.WithContext(rCtx), nil
}
// LookupRoute looks a route up and returns true when it is found.
func (c *Context) LookupRoute(request *http.Request) (*MatchedRoute, bool) {
if route, ok := c.router.Lookup(request.Method, request.URL.EscapedPath()); ok {
return route, ok
}
return nil, false
}
// RouteInfo tries to match a route for this request
// Returns the matched route, a shallow copy of the request if its context
// contains the matched router, otherwise the same request, and a bool to
// indicate if it the request matches one of the routes, if it doesn't
// then it returns false and nil for the other two return values.
func (c *Context) RouteInfo(request *http.Request) (*MatchedRoute, *http.Request, bool) {
var rCtx = request.Context()
if v, ok := rCtx.Value(ctxMatchedRoute).(*MatchedRoute); ok {
return v, request, ok
}
if route, ok := c.LookupRoute(request); ok {
rCtx = stdContext.WithValue(rCtx, ctxMatchedRoute, route)
return route, request.WithContext(rCtx), ok
}
return nil, nil, false
}
// ResponseFormat negotiates the response content type
// Returns the response format and a shallow copy of the request if its context
// doesn't contain the response format, otherwise the same request.
func (c *Context) ResponseFormat(r *http.Request, offers []string) (string, *http.Request) {
var rCtx = r.Context()
if v, ok := rCtx.Value(ctxResponseFormat).(string); ok {
c.debugLogf("[%s %s] found response format %q in context", r.Method, r.URL.Path, v)
return v, r
}
format := negotiate.ContentType(r, offers, "", c.negotiateOpts()...)
if format != "" {
c.debugLogf("[%s %s] set response format %q in context", r.Method, r.URL.Path, format)
r = r.WithContext(stdContext.WithValue(rCtx, ctxResponseFormat, format))
}
c.debugLogf("[%s %s] negotiated response format %q", r.Method, r.URL.Path, format)
return format, r
}
// AllowedMethods gets the allowed methods for the path of this request.
func (c *Context) AllowedMethods(request *http.Request) []string {
return c.router.OtherMethods(request.Method, request.URL.EscapedPath())
}
// ResetAuth removes the current principal from the request context.
func (c *Context) ResetAuth(request *http.Request) *http.Request {
rctx := request.Context()
rctx = stdContext.WithValue(rctx, ctxSecurityPrincipal, nil)
rctx = stdContext.WithValue(rctx, ctxSecurityScopes, nil)
return request.WithContext(rctx)
}
// BindAndValidate binds and validates the request
// Returns the validation map and a shallow copy of the request when its context
// doesn't contain the validation, otherwise it returns the same request or an
// CompositeValidationError error.
func (c *Context) BindAndValidate(request *http.Request, matched *MatchedRoute) (any, *http.Request, error) {
var rCtx = request.Context()
if v, ok := rCtx.Value(ctxBoundParams).(*validation); ok {
c.debugLogf("got cached validation (valid: %t)", len(v.result) == 0)
if len(v.result) > 0 {
return v.bound, request, errors.CompositeValidationError(v.result...)
}
return v.bound, request, nil
}
result := validateRequest(c, request, matched)
rCtx = stdContext.WithValue(rCtx, ctxBoundParams, result)
request = request.WithContext(rCtx)
if len(result.result) > 0 {
return result.bound, request, errors.CompositeValidationError(result.result...)
}
c.debugLogf("no validation errors found")
return result.bound, request, nil
}
// NotFound the default not found responder for when no route has been matched yet.
func (c *Context) NotFound(rw http.ResponseWriter, r *http.Request) {
c.Respond(rw, r, []string{c.api.DefaultProduces()}, nil, errors.NotFound("not found"))
}
// Respond renders the response after doing some content negotiation.
func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []string, route *MatchedRoute, data any) {
c.debugLogf("responding to %s %s with produces: %v", r.Method, r.URL.Path, produces)
offers := c.buildOffers(produces)
var format string
format, r = c.ResponseFormat(r, offers)
rw.Header().Set(runtime.HeaderContentType, format)
if resp, ok := data.(Responder); ok {
c.respondWithResponder(rw, r, route, resp, format)
return
}
if err, ok := data.(error); ok {
c.respondWithError(rw, r, produces, route, err, format)
return
}
if route == nil || route.Operation == nil {
c.respondWithoutCode(rw, r, data, format, offers)
return
}
if _, code, ok := route.Operation.SuccessResponse(); ok {
c.respondWithCode(rw, r, route, code, data, format)
return
}
c.api.ServeErrorFor(route.Operation.ID)(rw, r, fmt.Errorf("%d: %s", http.StatusInternalServerError, "can't produce response"))
}
// APIHandlerSwaggerUI returns a handler to serve the API.
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI ([docui.SwaggerUI]) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with combined [UIOption]).
//
// Deprecated: use [Context.APIHandlerWithUI] with [docui.SwaggerUI] middleware instead.
func (c *Context) APIHandlerSwaggerUI(builder Builder, opts ...UIOption) http.Handler {
return c.APIHandlerWithUI(builder, docui.UseSwaggerUI, c.uiOptionsForHandler(opts)...)
}
// APIHandlerRapiDoc returns a handler to serve the API.
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI ([docui.RapiDoc]) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with combined [UIOption]).
//
// Deprecated: use [Context.APIHandlerWithUI] with [docui.UseRapiDoc] middleware instead.
func (c *Context) APIHandlerRapiDoc(builder Builder, opts ...UIOption) http.Handler {
return c.APIHandlerWithUI(builder, docui.UseRapiDoc, c.uiOptionsForHandler(opts)...)
}
// APIHandler returns a handler to serve the API.
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI ([docui.Redoc]) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with combined [UIOption]).
//
// Notice that you may use [Context.APIHandlerWithUI] to use an alternate UI-serving middleware.
func (c *Context) APIHandler(builder Builder, opts ...UIOption) http.Handler {
return c.APIHandlerWithUI(builder, docui.UseRedoc, c.uiOptionsForHandler(opts)...)
}
// APIHandlerWithUI returns a handler to serve the API with a swagger spec and a UI.
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with combined [UIOption]).
//
// Notice that any function that accepts the [docui.Option] set and returns a valid middleware may be injected here.
//
// [Context.APIHandlerWithUI] extends [Context.APIHandler], and supersedes [Context.APIHandlerRapiDoc] and [Context.APIHandlerSwaggerUI].
func (c *Context) APIHandlerWithUI(builder Builder, uiMiddleware docui.UIMiddleware, opts ...docui.Option) http.Handler {
b := builder
if b == nil {
b = PassthroughBuilder
}
// the UI titles defaults to the title in the spec
const extraOptions = 2
prepend := make([]docui.Option, 0, len(opts)+extraOptions)
var title string
sp := c.spec.Spec()
if sp != nil && sp.Info != nil && sp.Info.Title != "" {
title = sp.Info.Title
}
if title != "" {
prepend = append(prepend, docui.WithUITitle(title))
}
prepend = append(prepend, docui.WithUIBasePath(c.BasePath()))
prepend = append(prepend, opts...)
// aligns spec serve path with UI setting to fetch spec document.
return docui.UseSpec(c.spec.Raw(), docui.WithSpecPathFromOptions(prepend...))(
uiMiddleware(prepend...)(
c.RoutesHandler(b),
),
)
}
// RoutesHandler returns a handler to serve the API, just the routes and the contract defined in the swagger spec.
func (c *Context) RoutesHandler(builder Builder) http.Handler {
b := builder
if b == nil {
b = PassthroughBuilder
}
return NewRouter(c, b(NewOperationExecutor(c)))
}
// authorizeImpl is the real authentication+authorization body shared
// between the production and dev-only variants of [Context.Authorize].
// See context_skipauth_disabled.go (default build) and
// context_skipauth_enabled.go (the `openapi_unsafe_skipauth` build tag).
//
// The doc on the exported Authorize describes the user-facing
// contract; this function MUST NOT change semantics for the
// production path.
func (c *Context) authorizeImpl(request *http.Request, route *MatchedRoute) (any, *http.Request, error) {
if route == nil || !route.HasAuth() {
return nil, nil, nil
}
var rCtx = request.Context()
if v := rCtx.Value(ctxSecurityPrincipal); v != nil {
return v, request, nil
}
applies, usr, err := route.Authenticators.Authenticate(request, route)
if !applies || err != nil || !route.Authenticators.AllowsAnonymous() && typeutils.IsZero(usr) {
if err != nil {
return nil, nil, err
}
return nil, nil, errors.Unauthenticated("invalid credentials")
}
if route.Authorizer != nil {
if err := route.Authorizer.Authorize(request, usr); err != nil {
var apiError errors.Error
if stderrors.As(err, &apiError) {
return nil, nil, err
}
return nil, nil, errors.New(http.StatusForbidden, "%v", err)
}
}
rCtx = request.Context()
rCtx = stdContext.WithValue(rCtx, ctxSecurityPrincipal, usr)
rCtx = stdContext.WithValue(rCtx, ctxSecurityScopes, route.Authenticator.AllScopes())
return usr, request.WithContext(rCtx), nil
}
func (c *Context) bindRequestBody(request *http.Request, route *MatchedRoute) (string, runtime.Consumer, error) {
ct, _, err := runtime.ContentType(request.Header)
if err != nil {
return "", nil, err
}
c.debugLogf("validating content type for %q against [%s]", ct, strings.Join(route.Consumes, ", "))
if err := validateContentType(route.Consumes, ct); err != nil {
return "", nil, err
}
cons, ok := mediatype.Lookup(route.Consumers, ct, c.matchOpts()...)
if !ok {
return "", nil, errors.New(http.StatusInternalServerError, "no consumer registered for %s", ct)
}
return ct, cons, nil
}
func (c *Context) respondWithResponder(rw http.ResponseWriter, r *http.Request, route *MatchedRoute, resp Responder, format string) {
_ = r
producers := route.Producers
// producers contains keys with normalized format, if a format has MIME type parameter such as `text/plain; charset=utf-8`
// then you must provide `text/plain` to get the correct producer. HOWEVER, format here is not normalized.
prod, ok := producers[normalizeOffer(format)]
if !ok {
prods := c.api.ProducersFor(normalizeOffers([]string{c.api.DefaultProduces()}))
pr, ok := prods[c.api.DefaultProduces()]
if !ok {
panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, cantFindProducer(format)))
}
prod = pr
}
resp.WriteResponse(rw, prod)
}
func (c *Context) respondWithError(rw http.ResponseWriter, r *http.Request, produces []string, route *MatchedRoute, err error, format string) {
_ = produces
if format == "" {
rw.Header().Set(runtime.HeaderContentType, runtime.JSONMime)
}
if realm := security.FailedBasicAuth(r); realm != "" {
rw.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", realm))
}
if route == nil || route.Operation == nil {
c.api.ServeErrorFor("")(rw, r, err)
return
}
c.api.ServeErrorFor(route.Operation.ID)(rw, r, err)
}
func (c *Context) respondWithoutCode(rw http.ResponseWriter, r *http.Request, data any, format string, offers []string) {
rw.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
producers := c.api.ProducersFor(normalizeOffers(offers))
prod, ok := producers[format]
if !ok {
panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, cantFindProducer(format)))
}
if err := prod.Produce(rw, data); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
func (c *Context) buildOffers(produces []string) []string {
offers := make([]string, 0, len(produces)+1)
for _, mt := range produces {
if mt != c.api.DefaultProduces() {
offers = append(offers, mt)
}
}
// the default producer is last so more specific producers take precedence
offers = append(offers, c.api.DefaultProduces())
c.debugLogf("offers: %v", offers)
return offers
}
func (c *Context) respondWithCode(rw http.ResponseWriter, r *http.Request, route *MatchedRoute, code int, data any, format string) {
rw.WriteHeader(code)
if code == http.StatusNoContent || r.Method == http.MethodHead {
return
}
producers := route.Producers
prod, ok := producers[format]
if !ok {
if !ok {
prods := c.api.ProducersFor(normalizeOffers([]string{c.api.DefaultProduces()}))
pr, ok := prods[c.api.DefaultProduces()]
if !ok {
panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, cantFindProducer(format)))
}
prod = pr
}
}
if err := prod.Produce(rw, data); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
// uiOptionsForHandler bridges the deprecated [UIOption] set to the new [docui.Option] set.
func (c Context) uiOptionsForHandler(opts []UIOption) []docui.Option {
uiOpts := uiOptionsWithDefaults(opts)
return uiOpts.toFuncOptions()
}
func (c *Context) negotiateOpts() []negotiate.Option {
var opts []negotiate.Option
if c.ignoreParameters {
opts = append(opts, negotiate.WithIgnoreParameters(true))
}
if c.matchSuffix {
opts = append(opts, negotiate.WithMatchSuffix(true))
}
return opts
}
// matchOpts builds the mediatype.MatchOption slice that the
// codec-lookup and Content-Type validation paths apply server-wide.
// Mirrors negotiateOpts but at the mediatype level (without going
// through the negotiate.Option wrapper).
func (c *Context) matchOpts() []mediatype.MatchOption {
if !c.matchSuffix {
return nil
}
return []mediatype.MatchOption{mediatype.AllowSuffix()}
}
func cantFindProducer(format string) string {
return "can't find a producer for " + format
}