Skip to content

feat: allow providing a Prefix for paths to validate #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
11 changes: 11 additions & 0 deletions oapi_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Comment on lines +166 to +167
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this should be a copy not the existing r, so we don't manipulate it before it goes into i.e. the error handler

r.URL.RawPath = strings.TrimPrefix(r.URL.RawPath, options.Prefix)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may not be needed


route, pathParams, err := router.FindRoute(r)
if err != nil {
errOpts := ErrorHandlerOpts{
Expand Down Expand Up @@ -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)
Comment on lines +234 to +235
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this should be a copy not the existing r, so we don't manipulate it before it goes into i.e. the error handler


// Find route
route, pathParams, err := router.FindRoute(r)
if err != nil {
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 @@ -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
}
Loading