Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions oapi_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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.
Expand Down Expand Up @@ -153,9 +156,26 @@ func performRequestValidationForErrorHandler(next http.Handler, w http.ResponseW
errorHandler(w, err.Error(), statusCode)
}

func makeRequestForValidation(r *http.Request, options *Options) *http.Request {
if options == nil || options.Prefix == "" {
return r
}

r = r.Clone(r.Context())

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)

return r
}

// 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 = makeRequestForValidation(r, options)

route, pathParams, err := router.FindRoute(r)
if err != nil {
errOpts := ErrorHandlerOpts{
Expand Down Expand Up @@ -220,6 +240,7 @@ func performRequestValidationForErrorHandlerWithOpts(next http.Handler, w http.R
// validateRequest is called from the middleware above and actually does the work
// of validating a request.
func validateRequest(r *http.Request, router routers.Router, options *Options) (int, error) {
r = makeRequestForValidation(r, options)

// Find route
route, pathParams, err := router.FindRoute(r)
Expand Down
118 changes: 118 additions & 0 deletions oapi_validate_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,3 +839,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
}