diff --git a/oapi_validate.go b/oapi_validate.go index cbe494e..209782c 100644 --- a/oapi_validate.go +++ b/oapi_validate.go @@ -103,6 +103,9 @@ type Options struct { SilenceServersWarning bool // DoNotValidateServers ensures that there is no Host validation performed (see `SilenceServersWarning` and https://github.com/deepmap/oapi-codegen/issues/882 for more details) DoNotValidateServers bool + // Prefix allows (optionally) trimming a prefix from the API path. + // This may be useful if your API is routed to an internal path that is different from the OpenAPI specification. + Prefix string } // OapiRequestValidator Creates the middleware to validate that incoming requests match the given OpenAPI 3.x spec, with a default set of configuration. @@ -159,6 +162,11 @@ func performRequestValidationForErrorHandler(next http.Handler, w http.ResponseW // Note that this is an inline-and-modified version of `validateRequest`, with a simplified control flow and providing full access to the `error` for the `ErrorHandlerWithOpts` function. func performRequestValidationForErrorHandlerWithOpts(next http.Handler, w http.ResponseWriter, r *http.Request, router routers.Router, options *Options) { // Find route + + r.RequestURI = strings.TrimPrefix(r.RequestURI, options.Prefix) + r.URL.Path = strings.TrimPrefix(r.URL.Path, options.Prefix) + r.URL.RawPath = strings.TrimPrefix(r.URL.RawPath, options.Prefix) + route, pathParams, err := router.FindRoute(r) if err != nil { errOpts := ErrorHandlerOpts{ @@ -223,6 +231,9 @@ func performRequestValidationForErrorHandlerWithOpts(next http.Handler, w http.R // of validating a request. func validateRequest(r *http.Request, router routers.Router, options *Options) (int, error) { + r.RequestURI = strings.TrimPrefix(r.RequestURI, options.Prefix) + r.URL.Path = strings.TrimPrefix(r.URL.Path, options.Prefix) + // Find route route, pathParams, err := router.FindRoute(r) if err != nil { diff --git a/oapi_validate_example_test.go b/oapi_validate_example_test.go index a14faa3..6d79efa 100644 --- a/oapi_validate_example_test.go +++ b/oapi_validate_example_test.go @@ -843,3 +843,121 @@ paths: // Received an HTTP 400 response. Expected HTTP 400 // Response body: There was a bad request } + +// In the case that your public OpenAPI spec documents an API which does /not/ match your internal API endpoint setup, you may want to set the `Prefix` option to allow rewriting paths +func ExampleOapiRequestValidatorWithOptions_withPrefix() { + rawSpec := ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: TestServer +servers: + - url: http://example.com/ +paths: + /resource: + post: + operationId: createResource + responses: + '204': + description: No content + requestBody: + required: true + content: + application/json: + schema: + properties: + name: + type: string + additionalProperties: false +` + + must := func(err error) { + if err != nil { + panic(err) + } + } + + use := func(r *http.ServeMux, middlewares ...func(next http.Handler) http.Handler) http.Handler { + var s http.Handler + s = r + + for _, mw := range middlewares { + s = mw(s) + } + + return s + } + + logResponseBody := func(rr *httptest.ResponseRecorder) { + if rr.Result().Body != nil { + data, _ := io.ReadAll(rr.Result().Body) + if len(data) > 0 { + fmt.Printf("Response body: %s", data) + } + } + } + + spec, err := openapi3.NewLoader().LoadFromData([]byte(rawSpec)) + must(err) + + // NOTE that we need to make sure that the `Servers` aren't set, otherwise the OpenAPI validation middleware will validate that the `Host` header (of incoming requests) are targeting known `Servers` in the OpenAPI spec + // See also: Options#SilenceServersWarning + spec.Servers = nil + + router := http.NewServeMux() + + // This should be treated as if it's being called with POST /resource + router.HandleFunc("/public-api/v1/resource", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s /public-api/v1/resource was called\n", r.Method) + + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusNoContent) + return + } + + w.WriteHeader(http.StatusMethodNotAllowed) + }) + + router.HandleFunc("/internal-api/v2/resource", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s /internal-api/v2/resource was called\n", r.Method) + + w.WriteHeader(http.StatusMethodNotAllowed) + }) + + // create middleware + mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{ + Options: openapi3filter.Options{ + // make sure that multiple errors in a given request are returned + MultiError: true, + }, + Prefix: "/public-api/v1/", + }) + + // then wire it in + server := use(router, mw) + + // ================================================================================ + fmt.Println("# A request that is well-formed is passed through to the Handler") + body := map[string]string{ + "name": "Jamie", + } + + data, err := json.Marshal(body) + must(err) + + req, err := http.NewRequest(http.MethodPost, "/public-api/v1/resource", bytes.NewReader(data)) + must(err) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + + server.ServeHTTP(rr, req) + + fmt.Printf("Received an HTTP %d response. Expected HTTP 204\n", rr.Code) + logResponseBody(rr) + + // Output: + // # A request that is well-formed is passed through to the Handler + // POST /public-api/v1/resource was called + // Received an HTTP 204 response. Expected HTTP 204 +}