// 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 }