├── .gitignore ├── .github ├── CODEOWNERS ├── release-drafter.yml ├── FUNDING.yml └── workflows │ ├── release-drafter.yml │ ├── ci.yml │ ├── lint.yml │ ├── tidy.yml │ └── generate.yml ├── renovate.json ├── go.mod ├── internal └── test │ ├── gorilla │ ├── go.mod │ ├── test_spec.yaml │ ├── go.sum │ └── oapi_validate_test.go │ ├── nethttp │ ├── go.mod │ ├── test_spec.yaml │ ├── go.sum │ └── oapi_validate_test.go │ └── chi │ ├── go.mod │ ├── test_spec.yaml │ ├── go.sum │ └── oapi_validate_test.go ├── Makefile ├── oapi_validate_test.go ├── go.sum ├── README.md ├── LICENSE ├── oapi_validate.go └── oapi_validate_example_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @oapi-codegen/maintainers 2 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - oapi-codegen 3 | - jamietanna 4 | tidelift: go/github.com/oapi-codegen/nethttp-middleware 5 | open_collective: oapi-codegen 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>oapi-codegen/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: {} 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | update_release_draft: 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 20 | with: 21 | name: next 22 | tag: next 23 | version: next 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oapi-codegen/nethttp-middleware 2 | 3 | go 1.22.5 4 | 5 | require github.com/getkin/kin-openapi v0.132.0 6 | 7 | require ( 8 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 9 | github.com/go-openapi/swag v0.23.0 // indirect 10 | github.com/gorilla/mux v1.8.1 // indirect 11 | github.com/josharian/intern v1.0.0 // indirect 12 | github.com/mailru/easyjson v0.7.7 // indirect 13 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 14 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 15 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 16 | github.com/perimeterx/marshmallow v1.1.5 // indirect 17 | github.com/ugorji/go/codec v1.2.11 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build project 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go 10 | matrix: 11 | version: 12 | # versions of Go that this module can still be built with (and therefore are "supported" by this project) 13 | - "1.22" 14 | # actively supported versions of Go 15 | - "1.23" 16 | - "1.24" 17 | steps: 18 | - name: Check out source code 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 23 | with: 24 | go-version: ${{ matrix.version }} 25 | 26 | - name: Test 27 | run: make test 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint project 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go 10 | matrix: 11 | version: 12 | # versions of Go that this module can still be built with (and therefore are "supported" by this project) 13 | - "1.22" 14 | # actively supported versions of Go 15 | - "1.23" 16 | - "1.24" 17 | steps: 18 | - name: Check out source code 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 23 | with: 24 | go-version: ${{ matrix.version }} 25 | 26 | - name: Run `make lint-ci` 27 | run: make lint-ci 28 | -------------------------------------------------------------------------------- /internal/test/gorilla/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oapi-codegen/nethttp-middleware/internal/test/gorilla 2 | 3 | go 1.22.5 4 | 5 | replace github.com/oapi-codegen/nethttp-middleware => ../../../ 6 | 7 | require ( 8 | github.com/getkin/kin-openapi v0.132.0 9 | github.com/gorilla/mux v1.8.1 10 | github.com/oapi-codegen/nethttp-middleware v0.0.0-00010101000000-000000000000 11 | github.com/stretchr/testify v1.9.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 17 | github.com/go-openapi/swag v0.23.0 // indirect 18 | github.com/josharian/intern v1.0.0 // indirect 19 | github.com/mailru/easyjson v0.7.7 // indirect 20 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 21 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 22 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 23 | github.com/perimeterx/marshmallow v1.1.5 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /internal/test/nethttp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oapi-codegen/nethttp-middleware/internal/test/nethttp 2 | 3 | go 1.22.5 4 | 5 | replace github.com/oapi-codegen/nethttp-middleware => ../../../ 6 | 7 | require ( 8 | github.com/getkin/kin-openapi v0.132.0 9 | github.com/oapi-codegen/nethttp-middleware v0.0.0-00010101000000-000000000000 10 | github.com/stretchr/testify v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/gorilla/mux v1.8.1 // indirect 18 | github.com/josharian/intern v1.0.0 // indirect 19 | github.com/mailru/easyjson v0.7.7 // indirect 20 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 21 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 22 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 23 | github.com/perimeterx/marshmallow v1.1.5 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /.github/workflows/tidy.yml: -------------------------------------------------------------------------------- 1 | name: Ensure `go mod tidy` has been run 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go 10 | matrix: 11 | version: 12 | # versions of Go that this module can still be built with (and therefore are "supported" by this project) 13 | - "1.22" 14 | # actively supported versions of Go 15 | - "1.23" 16 | - "1.24" 17 | steps: 18 | - name: Check out source code 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 23 | with: 24 | go-version: ${{ matrix.version }} 25 | 26 | - name: Install `tidied` 27 | run: go install gitlab.com/jamietanna/tidied@latest 28 | 29 | - name: Check for no untracked files 30 | run: tidied -verbose 31 | -------------------------------------------------------------------------------- /.github/workflows/generate.yml: -------------------------------------------------------------------------------- 1 | name: Ensure generated files are up-to-date 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go 10 | matrix: 11 | version: 12 | # versions of Go that this module can still be built with (and therefore are "supported" by this project) 13 | - "1.22" 14 | # actively supported versions of Go 15 | - "1.23" 16 | - "1.24" 17 | steps: 18 | - name: Check out source code 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 23 | with: 24 | go-version: ${{ matrix.version }} 25 | 26 | - name: Run `make generate` 27 | run: make generate 28 | 29 | - name: Check for no untracked files 30 | run: git status && git diff-index --quiet HEAD -- 31 | -------------------------------------------------------------------------------- /internal/test/chi/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oapi-codegen/nethttp-middleware/internal/test/chi 2 | 3 | go 1.22.5 4 | 5 | replace github.com/oapi-codegen/nethttp-middleware => ../../../ 6 | 7 | require ( 8 | github.com/getkin/kin-openapi v0.132.0 9 | github.com/go-chi/chi/v5 v5.0.10 10 | github.com/oapi-codegen/nethttp-middleware v0.0.0-00010101000000-000000000000 11 | github.com/stretchr/testify v1.9.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 17 | github.com/go-openapi/swag v0.23.0 // indirect 18 | github.com/gorilla/mux v1.8.1 // indirect 19 | github.com/josharian/intern v1.0.0 // indirect 20 | github.com/mailru/easyjson v0.7.7 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 23 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 24 | github.com/perimeterx/marshmallow v1.1.5 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOBASE=$(shell pwd) 2 | GOBIN=$(GOBASE)/bin 3 | 4 | help: 5 | @echo "This is a helper makefile for oapi-codegen" 6 | @echo "Targets:" 7 | @echo " generate: regenerate all generated files" 8 | @echo " test: run all tests" 9 | @echo " gin_example generate gin example server code" 10 | @echo " tidy tidy go mod" 11 | 12 | $(GOBIN)/golangci-lint: 13 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v2.1.6 14 | 15 | .PHONY: tools 16 | tools: $(GOBIN)/golangci-lint 17 | 18 | lint: tools 19 | git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && $(GOBIN)/golangci-lint run ./...' 20 | 21 | lint-ci: tools 22 | git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m' 23 | 24 | generate: 25 | git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && go generate ./...' 26 | 27 | test: 28 | git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && go test -cover ./...' 29 | 30 | tidy: 31 | @echo "tidy..." 32 | git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && go mod tidy' 33 | -------------------------------------------------------------------------------- /oapi_validate_test.go: -------------------------------------------------------------------------------- 1 | package nethttpmiddleware 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/getkin/kin-openapi/openapi3filter" 8 | ) 9 | 10 | func Test_determineStatusCodeForMultiError(t *testing.T) { 11 | t.Run("returns HTTP 400 Bad Request when only `RequestError`s", func(t *testing.T) { 12 | errs := []error{ 13 | &openapi3filter.RequestError{}, 14 | &openapi3filter.RequestError{}, 15 | } 16 | 17 | expected := 400 18 | actual := determineStatusCodeForMultiError(errs) 19 | 20 | if expected != actual { 21 | t.Errorf("Expected an HTTP %d to be returned, but received %d", expected, actual) 22 | } 23 | }) 24 | 25 | t.Run("returns HTTP 401 Unauthorized when only `SecurityRequirementsError`s", func(t *testing.T) { 26 | errs := []error{ 27 | &openapi3filter.SecurityRequirementsError{}, 28 | &openapi3filter.SecurityRequirementsError{}, 29 | } 30 | 31 | expected := 401 32 | actual := determineStatusCodeForMultiError(errs) 33 | 34 | if expected != actual { 35 | t.Errorf("Expected an HTTP %d to be returned, but received %d", expected, actual) 36 | } 37 | }) 38 | 39 | t.Run("returns HTTP 500 Internal Server Error when mixed error types", func(t *testing.T) { 40 | errs := []error{ 41 | &openapi3filter.RequestError{}, 42 | &openapi3filter.SecurityRequirementsError{}, 43 | } 44 | 45 | expected := 500 46 | actual := determineStatusCodeForMultiError(errs) 47 | 48 | if expected != actual { 49 | t.Errorf("Expected an HTTP %d to be returned, but received %d", expected, actual) 50 | } 51 | }) 52 | 53 | t.Run("returns HTTP 500 Internal Server Error when unknown error type(s) are seen", func(t *testing.T) { 54 | errs := []error{ 55 | fmt.Errorf("this isn't a known error type"), 56 | } 57 | 58 | expected := 500 59 | actual := determineStatusCodeForMultiError(errs) 60 | 61 | if expected != actual { 62 | t.Errorf("Expected an HTTP %d to be returned, but received %d", expected, actual) 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /internal/test/chi/test_spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: TestServer 5 | servers: 6 | - url: http://deepmap.ai/ 7 | paths: 8 | /resource: 9 | get: 10 | operationId: getResource 11 | parameters: 12 | - name: id 13 | in: query 14 | schema: 15 | type: integer 16 | minimum: 10 17 | maximum: 100 18 | responses: 19 | '200': 20 | description: success 21 | content: 22 | application/json: 23 | schema: 24 | properties: 25 | name: 26 | type: string 27 | id: 28 | type: integer 29 | post: 30 | operationId: createResource 31 | responses: 32 | '204': 33 | description: No content 34 | requestBody: 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | properties: 40 | name: 41 | type: string 42 | /protected_resource: 43 | get: 44 | operationId: getProtectedResource 45 | security: 46 | - BearerAuth: 47 | - someScope 48 | responses: 49 | '204': 50 | description: no content 51 | /protected_resource2: 52 | get: 53 | operationId: getProtectedResource 54 | security: 55 | - BearerAuth: 56 | - otherScope 57 | responses: 58 | '204': 59 | description: no content 60 | /protected_resource_401: 61 | get: 62 | operationId: getProtectedResource 63 | security: 64 | - BearerAuth: 65 | - unauthorized 66 | responses: 67 | '401': 68 | description: no content 69 | /multiparamresource: 70 | get: 71 | operationId: getResource 72 | parameters: 73 | - name: id 74 | in: query 75 | required: true 76 | schema: 77 | type: integer 78 | minimum: 10 79 | maximum: 100 80 | - name: id2 81 | required: true 82 | in: query 83 | schema: 84 | type: integer 85 | minimum: 10 86 | maximum: 100 87 | responses: 88 | '200': 89 | description: success 90 | content: 91 | application/json: 92 | schema: 93 | properties: 94 | name: 95 | type: string 96 | id: 97 | type: integer 98 | components: 99 | securitySchemes: 100 | BearerAuth: 101 | type: http 102 | scheme: bearer 103 | bearerFormat: JWT 104 | -------------------------------------------------------------------------------- /internal/test/gorilla/test_spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: TestServer 5 | servers: 6 | - url: http://deepmap.ai/ 7 | paths: 8 | /resource: 9 | get: 10 | operationId: getResource 11 | parameters: 12 | - name: id 13 | in: query 14 | schema: 15 | type: integer 16 | minimum: 10 17 | maximum: 100 18 | responses: 19 | '200': 20 | description: success 21 | content: 22 | application/json: 23 | schema: 24 | properties: 25 | name: 26 | type: string 27 | id: 28 | type: integer 29 | post: 30 | operationId: createResource 31 | responses: 32 | '204': 33 | description: No content 34 | requestBody: 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | properties: 40 | name: 41 | type: string 42 | /protected_resource: 43 | get: 44 | operationId: getProtectedResource 45 | security: 46 | - BearerAuth: 47 | - someScope 48 | responses: 49 | '204': 50 | description: no content 51 | /protected_resource2: 52 | get: 53 | operationId: getProtectedResource 54 | security: 55 | - BearerAuth: 56 | - otherScope 57 | responses: 58 | '204': 59 | description: no content 60 | /protected_resource_401: 61 | get: 62 | operationId: getProtectedResource 63 | security: 64 | - BearerAuth: 65 | - unauthorized 66 | responses: 67 | '401': 68 | description: no content 69 | /multiparamresource: 70 | get: 71 | operationId: getResource 72 | parameters: 73 | - name: id 74 | in: query 75 | required: true 76 | schema: 77 | type: integer 78 | minimum: 10 79 | maximum: 100 80 | - name: id2 81 | required: true 82 | in: query 83 | schema: 84 | type: integer 85 | minimum: 10 86 | maximum: 100 87 | responses: 88 | '200': 89 | description: success 90 | content: 91 | application/json: 92 | schema: 93 | properties: 94 | name: 95 | type: string 96 | id: 97 | type: integer 98 | components: 99 | securitySchemes: 100 | BearerAuth: 101 | type: http 102 | scheme: bearer 103 | bearerFormat: JWT 104 | -------------------------------------------------------------------------------- /internal/test/nethttp/test_spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: TestServer 5 | servers: 6 | - url: http://deepmap.ai/ 7 | paths: 8 | /resource: 9 | get: 10 | operationId: getResource 11 | parameters: 12 | - name: id 13 | in: query 14 | schema: 15 | type: integer 16 | minimum: 10 17 | maximum: 100 18 | responses: 19 | '200': 20 | description: success 21 | content: 22 | application/json: 23 | schema: 24 | properties: 25 | name: 26 | type: string 27 | id: 28 | type: integer 29 | post: 30 | operationId: createResource 31 | responses: 32 | '204': 33 | description: No content 34 | requestBody: 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | properties: 40 | name: 41 | type: string 42 | /protected_resource: 43 | get: 44 | operationId: getProtectedResource 45 | security: 46 | - BearerAuth: 47 | - someScope 48 | responses: 49 | '204': 50 | description: no content 51 | /protected_resource2: 52 | get: 53 | operationId: getProtectedResource 54 | security: 55 | - BearerAuth: 56 | - otherScope 57 | responses: 58 | '204': 59 | description: no content 60 | /protected_resource_401: 61 | get: 62 | operationId: getProtectedResource 63 | security: 64 | - BearerAuth: 65 | - unauthorized 66 | responses: 67 | '401': 68 | description: no content 69 | /multiparamresource: 70 | get: 71 | operationId: getResource 72 | parameters: 73 | - name: id 74 | in: query 75 | required: true 76 | schema: 77 | type: integer 78 | minimum: 10 79 | maximum: 100 80 | - name: id2 81 | required: true 82 | in: query 83 | schema: 84 | type: integer 85 | minimum: 10 86 | maximum: 100 87 | responses: 88 | '200': 89 | description: success 90 | content: 91 | application/json: 92 | schema: 93 | properties: 94 | name: 95 | type: string 96 | id: 97 | type: integer 98 | components: 99 | securitySchemes: 100 | BearerAuth: 101 | type: http 102 | scheme: bearer 103 | bearerFormat: JWT 104 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= 4 | github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= 5 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 6 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 7 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 8 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 9 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 10 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 11 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 12 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 13 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 14 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 15 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 16 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 19 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 20 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 22 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 23 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= 24 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= 25 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= 26 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 27 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 28 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 32 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 33 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 34 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 35 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 36 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 40 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 41 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | -------------------------------------------------------------------------------- /internal/test/gorilla/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= 4 | github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= 5 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 6 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 7 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 8 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 9 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 10 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 11 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 12 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 13 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 14 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 15 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 16 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 19 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 20 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 22 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 23 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= 24 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= 25 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= 26 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 27 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 28 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 32 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 33 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 34 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 35 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 36 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 40 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 41 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | -------------------------------------------------------------------------------- /internal/test/nethttp/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= 4 | github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= 5 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 6 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 7 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 8 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 9 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 10 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 11 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 12 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 13 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 14 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 15 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 16 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 19 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 20 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 22 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 23 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= 24 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= 25 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= 26 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 27 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 28 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 32 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 33 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 34 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 35 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 36 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 40 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 41 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | -------------------------------------------------------------------------------- /internal/test/chi/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= 4 | github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= 5 | github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 6 | github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 7 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 8 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 9 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 10 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 11 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 12 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 13 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 14 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 15 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 16 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 22 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 23 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 24 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 25 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= 26 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= 27 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= 28 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 29 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 30 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 34 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 35 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 36 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 37 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 38 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 41 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Validation Middleware for `net/http`-compatible servers 2 | 3 | An HTTP middleware to perform validation of incoming requests via an OpenAPI specification. 4 | 5 | This project is a lightweight wrapper over the excellent [kin-openapi](https://github.com/getkin/kin-openapi) library's [`openapi3filter` package](https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3filter). 6 | 7 | This is _intended_ to be used with code that's generated through [`oapi-codegen`](https://github.com/oapi-codegen/oapi-codegen), but should work otherwise. 8 | 9 | ⚠️ This README may be for the latest development version, which may contain unreleased changes. Please ensure you're looking at the README for the latest release version. 10 | 11 | ## Usage 12 | 13 | You can add the middleware to your project with: 14 | 15 | ```sh 16 | go get github.com/oapi-codegen/nethttp-middleware 17 | ``` 18 | 19 | There is a full example of usage in [the Go doc for this project](https://pkg.go.dev/github.com/oapi-codegen/nethttp-middleware#pkg-examples). 20 | 21 | A simplified version of this code is as follows: 22 | 23 | ```go 24 | rawSpec := ` 25 | openapi: "3.0.0" 26 | # ... 27 | ` 28 | spec, _ := openapi3.NewLoader().LoadFromData([]byte(rawSpec)) 29 | 30 | // 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 31 | // See also: Options#SilenceServersWarning 32 | spec.Servers = nil 33 | 34 | router := http.NewServeMux() 35 | 36 | router.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 37 | fmt.Printf("%s /resource was called\n", r.Method) 38 | 39 | if r.Method == http.MethodPost { 40 | w.WriteHeader(http.StatusNoContent) 41 | return 42 | } 43 | 44 | w.WriteHeader(http.StatusMethodNotAllowed) 45 | }) 46 | 47 | use := func(r *http.ServeMux, middlewares ...func(next http.Handler) http.Handler) http.Handler { 48 | var s http.Handler 49 | s = r 50 | 51 | for _, mw := range middlewares { 52 | s = mw(s) 53 | } 54 | 55 | return s 56 | } 57 | 58 | // create middleware 59 | mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{ 60 | Options: openapi3filter.Options{ 61 | AuthenticationFunc: authenticationFunc, 62 | }, 63 | }) 64 | 65 | // then wire it in 66 | server := use(router, mw) 67 | 68 | // now all HTTP routes will be handled by the middleware, and any requests that are invalid will be rejected 69 | ``` 70 | 71 | ## FAQs 72 | 73 | ### Which HTTP servers should this work with? 74 | 75 | If you're using something that's compliant with `net/http` (which should be all Go web frameworks / routers / HTTP servers) it should work as-is. 76 | 77 | We explicitly test with the following servers, as they correspond with versions used by users of [oapi-codegen/oapi-codegen](https://github.com/oapi-codegen/oapi-codegen): 78 | 79 | - [Chi](https://github.com/go-chi/chi) 80 | - [gorilla/mux](https://github.com/gorilla/mux) 81 | - [net/http](https://pkg.go.dev/net/http) 82 | 83 | ### "This doesn't support ..." / "I think it's a bug that ..." 84 | 85 | As this project is a lightweight wrapper over [kin-openapi](https://github.com/getkin/kin-openapi)'s [`openapi3filter` package](https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3filter), it's _likely_ that any bugs/features are better sent upstream. 86 | 87 | However, it's worth raising an issue here instead, as it'll allow us to triage it before it goes to the kin-openapi maintainers. 88 | 89 | Additionally, as `oapi-codegen` contains [a number of middleware modules](https://github.com/search?q=org%3Aoapi-codegen+middleware&type=repositories), we'll very likely want to implement the same functionality across all the middlewares, so it may take a bit more coordination to get the changes in across our middlewares. 90 | 91 | ### I've just updated my version of `kin-openapi`, and now I can't build my code 😠 92 | 93 | The [kin-openapi](https://github.com/getkin/kin-openapi) project - which we 💜 for providing a great library and set of tooling for interacting with OpenAPI - is a pre-v1 release, which means that they're within their rights to push breaking changes. 94 | 95 | This may lead to breakage in your consuming code, and if so, sorry that's happened! 96 | 97 | We'll be aware of the issue, and will work to update both the core `oapi-codegen` and the middlewares accordingly. 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /oapi_validate.go: -------------------------------------------------------------------------------- 1 | // Provide HTTP middleware functionality to validate that incoming requests conform to a given OpenAPI 3.x specification. 2 | // 3 | // This provides middleware for any `net/http` conforming HTTP Server. 4 | // 5 | // This package is a lightweight wrapper over https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3filter from https://pkg.go.dev/github.com/getkin/kin-openapi. 6 | // 7 | // This is _intended_ to be used with code that's generated through https://pkg.go.dev/github.com/oapi-codegen/oapi-codegen, but should work otherwise. 8 | package nethttpmiddleware 9 | 10 | import ( 11 | "context" 12 | "errors" 13 | "fmt" 14 | "log" 15 | "net/http" 16 | "strings" 17 | 18 | "github.com/getkin/kin-openapi/openapi3" 19 | "github.com/getkin/kin-openapi/openapi3filter" 20 | "github.com/getkin/kin-openapi/routers" 21 | "github.com/getkin/kin-openapi/routers/gorillamux" 22 | ) 23 | 24 | // ErrorHandler is called when there is an error in validation 25 | // 26 | // If both an `ErrorHandlerWithOpts` and `ErrorHandler` are set, the `ErrorHandlerWithOpts` takes precedence. 27 | // 28 | // Deprecated: it's recommended you migrate to the ErrorHandlerWithOpts, as it provides more control over how to handle an error that occurs, including giving direct access to the `error` itself. There are no plans to remove this method. 29 | type ErrorHandler func(w http.ResponseWriter, message string, statusCode int) 30 | 31 | // ErrorHandlerWithOpts is called when there is an error in validation, with more information about the `error` that occurred and which request is currently being processed. 32 | // 33 | // There are a number of known types that the `error` can be: 34 | // 35 | // - `*openapi3filter.SecurityRequirementsError` - if the `AuthenticationFunc` has failed to authenticate the request 36 | // - `*openapi3filter.RequestError` - if a bad request has been made 37 | // 38 | // Additionally, if you have set `openapi3filter.Options#MultiError`: 39 | // 40 | // - `openapi3.MultiError` (https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3#MultiError) 41 | // 42 | // If both an `ErrorHandlerWithOpts` and `ErrorHandler` are set, the `ErrorHandlerWithOpts` takes precedence. 43 | // 44 | // NOTE that this should ideally be used instead of ErrorHandler 45 | type ErrorHandlerWithOpts func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request, opts ErrorHandlerOpts) 46 | 47 | // ErrorHandlerOpts contains additional options that are passed to the `ErrorHandlerWithOpts` function in the case of an error being returned by the middleware 48 | type ErrorHandlerOpts struct { 49 | // StatusCode indicates the HTTP Status Code that the OpenAPI validation middleware _suggests_ is returned to the user. 50 | // 51 | // NOTE that this is very much a suggestion, and can be overridden if you believe you have a better approach. 52 | StatusCode int 53 | 54 | // MatchedRoute is the underlying path that this request is being matched against. 55 | // 56 | // This is the route according to the OpenAPI validation middleware, and can be used in addition to/instead of the `http.Request` 57 | // 58 | // NOTE that this will be nil if there is no matched route (i.e. a request has been sent to an endpoint not in the OpenAPI spec) 59 | MatchedRoute *ErrorHandlerOptsMatchedRoute 60 | } 61 | 62 | type ErrorHandlerOptsMatchedRoute struct { 63 | // Route indicates the Route that this error is received by. 64 | // 65 | // This can be used in addition to/instead of the `http.Request`. 66 | Route *routers.Route 67 | 68 | // PathParams are any path parameters that are determined from the request. 69 | // 70 | // This can be used in addition to/instead of the `http.Request`. 71 | PathParams map[string]string 72 | } 73 | 74 | // MultiErrorHandler is called when the OpenAPI filter returns an openapi3.MultiError (https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3#MultiError) 75 | type MultiErrorHandler func(openapi3.MultiError) (int, error) 76 | 77 | // Options allows configuring the OapiRequestValidator. 78 | type Options struct { 79 | // Options contains any configuration for the underlying `openapi3filter` 80 | Options openapi3filter.Options 81 | // ErrorHandler is called when a validation error occurs. 82 | // 83 | // If both an `ErrorHandlerWithOpts` and `ErrorHandler` are set, the `ErrorHandlerWithOpts` takes precedence. 84 | // 85 | // If not provided, `http.Error` will be called 86 | ErrorHandler ErrorHandler 87 | 88 | // ErrorHandlerWithOpts is called when there is an error in validation. 89 | // 90 | // If both an `ErrorHandlerWithOpts` and `ErrorHandler` are set, the `ErrorHandlerWithOpts` takes precedence. 91 | ErrorHandlerWithOpts ErrorHandlerWithOpts 92 | 93 | // MultiErrorHandler is called when there is an openapi3.MultiError (https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3#MultiError) returned by the `openapi3filter`. 94 | // 95 | // If not provided `defaultMultiErrorHandler` will be used. 96 | // 97 | // Does not get called when using `ErrorHandlerWithOpts` 98 | MultiErrorHandler MultiErrorHandler 99 | // SilenceServersWarning allows silencing a warning for https://github.com/deepmap/oapi-codegen/issues/882 that reports when an OpenAPI spec has `spec.Servers != nil` 100 | SilenceServersWarning bool 101 | // DoNotValidateServers ensures that there is no Host validation performed (see `SilenceServersWarning` and https://github.com/deepmap/oapi-codegen/issues/882 for more details) 102 | DoNotValidateServers bool 103 | } 104 | 105 | // OapiRequestValidator Creates the middleware to validate that incoming requests match the given OpenAPI 3.x spec, with a default set of configuration. 106 | func OapiRequestValidator(spec *openapi3.T) func(next http.Handler) http.Handler { 107 | return OapiRequestValidatorWithOptions(spec, nil) 108 | } 109 | 110 | // OapiRequestValidatorWithOptions Creates the middleware to validate that incoming requests match the given OpenAPI 3.x spec, allowing explicit configuration. 111 | // 112 | // NOTE that this may panic if the OpenAPI spec isn't valid, or if it cannot be used to create the middleware 113 | func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) func(next http.Handler) http.Handler { 114 | if options != nil && options.DoNotValidateServers { 115 | spec.Servers = nil 116 | } 117 | 118 | if spec.Servers != nil && (options == nil || !options.SilenceServersWarning) { 119 | log.Println("WARN: OapiRequestValidatorWithOptions called with an OpenAPI spec that has `Servers` set. This may lead to an HTTP 400 with `no matching operation was found` when sending a valid request, as the validator performs `Host` header validation. If you're expecting `Host` header validation, you can silence this warning by setting `Options.SilenceServersWarning = true`. See https://github.com/deepmap/oapi-codegen/issues/882 for more information.") 120 | } 121 | 122 | router, err := gorillamux.NewRouter(spec) 123 | if err != nil { 124 | panic(err) 125 | } 126 | 127 | return func(next http.Handler) http.Handler { 128 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 129 | if options == nil { 130 | performRequestValidationForErrorHandler(next, w, r, router, options, http.Error) 131 | } else if options.ErrorHandlerWithOpts != nil { 132 | performRequestValidationForErrorHandlerWithOpts(next, w, r, router, options) 133 | } else if options.ErrorHandler != nil { 134 | performRequestValidationForErrorHandler(next, w, r, router, options, options.ErrorHandler) 135 | } else { 136 | // NOTE that this shouldn't happen, but let's be sure that we always end up calling the default error handler if no other handler is defined 137 | performRequestValidationForErrorHandler(next, w, r, router, options, http.Error) 138 | } 139 | }) 140 | } 141 | 142 | } 143 | 144 | func performRequestValidationForErrorHandler(next http.Handler, w http.ResponseWriter, r *http.Request, router routers.Router, options *Options, errorHandler ErrorHandler) { 145 | // validate request 146 | statusCode, err := validateRequest(r, router, options) 147 | if err == nil { 148 | // serve 149 | next.ServeHTTP(w, r) 150 | return 151 | } 152 | 153 | errorHandler(w, err.Error(), statusCode) 154 | } 155 | 156 | // 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. 157 | func performRequestValidationForErrorHandlerWithOpts(next http.Handler, w http.ResponseWriter, r *http.Request, router routers.Router, options *Options) { 158 | // Find route 159 | route, pathParams, err := router.FindRoute(r) 160 | if err != nil { 161 | errOpts := ErrorHandlerOpts{ 162 | // MatchedRoute will be nil, as we've not matched a route we know about 163 | StatusCode: http.StatusNotFound, 164 | } 165 | 166 | options.ErrorHandlerWithOpts(r.Context(), err, w, r, errOpts) 167 | return 168 | } 169 | 170 | errOpts := ErrorHandlerOpts{ 171 | MatchedRoute: &ErrorHandlerOptsMatchedRoute{ 172 | Route: route, 173 | PathParams: pathParams, 174 | }, 175 | // other options will be added before executing 176 | } 177 | 178 | // Validate request 179 | requestValidationInput := &openapi3filter.RequestValidationInput{ 180 | Request: r, 181 | PathParams: pathParams, 182 | Route: route, 183 | } 184 | 185 | if options != nil { 186 | requestValidationInput.Options = &options.Options 187 | } 188 | 189 | err = openapi3filter.ValidateRequest(r.Context(), requestValidationInput) 190 | if err == nil { 191 | // it's a valid request, so serve it 192 | next.ServeHTTP(w, r) 193 | return 194 | } 195 | 196 | var theErr error 197 | 198 | switch e := err.(type) { 199 | case openapi3.MultiError: 200 | theErr = e 201 | errOpts.StatusCode = determineStatusCodeForMultiError(e) 202 | case *openapi3filter.RequestError: 203 | // We've got a bad request 204 | theErr = e 205 | errOpts.StatusCode = http.StatusBadRequest 206 | case *openapi3filter.SecurityRequirementsError: 207 | theErr = e 208 | errOpts.StatusCode = http.StatusUnauthorized 209 | default: 210 | // This should never happen today, but if our upstream code changes, 211 | // we don't want to crash the server, so handle the unexpected error. 212 | // return http.StatusInternalServerError, 213 | theErr = fmt.Errorf("error validating route: %w", e) 214 | errOpts.StatusCode = http.StatusInternalServerError 215 | } 216 | 217 | options.ErrorHandlerWithOpts(r.Context(), theErr, w, r, errOpts) 218 | } 219 | 220 | // validateRequest is called from the middleware above and actually does the work 221 | // of validating a request. 222 | func validateRequest(r *http.Request, router routers.Router, options *Options) (int, error) { 223 | 224 | // Find route 225 | route, pathParams, err := router.FindRoute(r) 226 | if err != nil { 227 | if errors.Is(err, routers.ErrMethodNotAllowed) { 228 | return http.StatusMethodNotAllowed, err 229 | } 230 | 231 | return http.StatusNotFound, err // We failed to find a matching route for the request. 232 | } 233 | 234 | // Validate request 235 | requestValidationInput := &openapi3filter.RequestValidationInput{ 236 | Request: r, 237 | PathParams: pathParams, 238 | Route: route, 239 | } 240 | 241 | if options != nil { 242 | requestValidationInput.Options = &options.Options 243 | } 244 | 245 | if err := openapi3filter.ValidateRequest(r.Context(), requestValidationInput); err != nil { 246 | me := openapi3.MultiError{} 247 | if errors.As(err, &me) { 248 | errFunc := getMultiErrorHandlerFromOptions(options) 249 | return errFunc(me) 250 | } 251 | 252 | switch e := err.(type) { 253 | case *openapi3filter.RequestError: 254 | // We've got a bad request 255 | // Split up the verbose error by lines and return the first one 256 | // openapi errors seem to be multi-line with a decent message on the first 257 | errorLines := strings.Split(e.Error(), "\n") 258 | return http.StatusBadRequest, fmt.Errorf(errorLines[0]) 259 | case *openapi3filter.SecurityRequirementsError: 260 | return http.StatusUnauthorized, err 261 | default: 262 | // This should never happen today, but if our upstream code changes, 263 | // we don't want to crash the server, so handle the unexpected error. 264 | return http.StatusInternalServerError, fmt.Errorf("error validating route: %s", err.Error()) 265 | } 266 | } 267 | 268 | return http.StatusOK, nil 269 | } 270 | 271 | // attempt to get the MultiErrorHandler from the options. If it is not set, 272 | // return a default handler 273 | func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler { 274 | if options == nil { 275 | return defaultMultiErrorHandler 276 | } 277 | 278 | if options.MultiErrorHandler == nil { 279 | return defaultMultiErrorHandler 280 | } 281 | 282 | return options.MultiErrorHandler 283 | } 284 | 285 | // defaultMultiErrorHandler returns a StatusBadRequest (400) and a list 286 | // of all the errors. This method is called if there are no other 287 | // methods defined on the options. 288 | func defaultMultiErrorHandler(me openapi3.MultiError) (int, error) { 289 | return http.StatusBadRequest, me 290 | } 291 | 292 | func determineStatusCodeForMultiError(errs openapi3.MultiError) int { 293 | numRequestErrors := 0 294 | numSecurityRequirementsErrors := 0 295 | 296 | for _, err := range errs { 297 | switch err.(type) { 298 | case *openapi3filter.RequestError: 299 | numRequestErrors++ 300 | case *openapi3filter.SecurityRequirementsError: 301 | numSecurityRequirementsErrors++ 302 | default: 303 | // if we have /any/ unknown error types, we should suggest returning an HTTP 500 Internal Server Error 304 | return http.StatusInternalServerError 305 | } 306 | } 307 | 308 | if numRequestErrors > 0 && numSecurityRequirementsErrors > 0 { 309 | return http.StatusInternalServerError 310 | } 311 | 312 | if numRequestErrors > 0 { 313 | return http.StatusBadRequest 314 | } 315 | 316 | if numSecurityRequirementsErrors > 0 { 317 | return http.StatusUnauthorized 318 | } 319 | 320 | // we shouldn't hit this, but to be safe, return an HTTP 500 Internal Server Error if we don't have any cases above 321 | return http.StatusInternalServerError 322 | } 323 | -------------------------------------------------------------------------------- /internal/test/chi/oapi_validate_test.go: -------------------------------------------------------------------------------- 1 | package chi 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "testing" 14 | 15 | middleware "github.com/oapi-codegen/nethttp-middleware" 16 | 17 | "github.com/getkin/kin-openapi/openapi3" 18 | "github.com/getkin/kin-openapi/openapi3filter" 19 | "github.com/go-chi/chi/v5" 20 | "github.com/stretchr/testify/assert" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | //go:embed test_spec.yaml 25 | var testSchema []byte 26 | 27 | func doGet(t *testing.T, mux http.Handler, rawURL string) *httptest.ResponseRecorder { 28 | u, err := url.Parse(rawURL) 29 | if err != nil { 30 | t.Fatalf("Invalid url: %s", rawURL) 31 | } 32 | 33 | req, err := http.NewRequest(http.MethodGet, u.String(), nil) 34 | require.NoError(t, err) 35 | 36 | req.Header.Set("accept", "application/json") 37 | 38 | rr := httptest.NewRecorder() 39 | 40 | mux.ServeHTTP(rr, req) 41 | 42 | return rr 43 | } 44 | 45 | func doPost(t *testing.T, mux http.Handler, rawURL string, jsonBody interface{}) *httptest.ResponseRecorder { 46 | u, err := url.Parse(rawURL) 47 | if err != nil { 48 | t.Fatalf("Invalid url: %s", rawURL) 49 | } 50 | 51 | data, err := json.Marshal(jsonBody) 52 | require.NoError(t, err) 53 | 54 | req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(data)) 55 | require.NoError(t, err) 56 | 57 | req.Header.Set("content-type", "application/json") 58 | 59 | rr := httptest.NewRecorder() 60 | 61 | mux.ServeHTTP(rr, req) 62 | 63 | return rr 64 | } 65 | 66 | func TestOapiRequestValidator(t *testing.T) { 67 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 68 | require.NoError(t, err, "Error initializing OpenAPI spec") 69 | 70 | r := chi.NewRouter() 71 | 72 | // register middleware 73 | r.Use(middleware.OapiRequestValidator(spec)) 74 | 75 | // basic cases 76 | testRequestValidatorBasicFunctions(t, r) 77 | } 78 | 79 | func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) { 80 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 81 | require.NoError(t, err, "Error initializing OpenAPI spec") 82 | 83 | r := chi.NewRouter() 84 | 85 | // Set up an authenticator to check authenticated function. It will allow 86 | // access to "someScope", but disallow others. 87 | options := middleware.Options{ 88 | Options: openapi3filter.Options{ 89 | ExcludeRequestBody: false, 90 | ExcludeResponseBody: false, 91 | IncludeResponseStatus: true, 92 | MultiError: true, 93 | }, 94 | } 95 | 96 | // register middleware 97 | r.Use(middleware.OapiRequestValidatorWithOptions(spec, &options)) 98 | 99 | called := false 100 | 101 | // Install a request handler for /resource. We want to make sure it doesn't 102 | // get called. 103 | r.Get("/multiparamresource", func(w http.ResponseWriter, r *http.Request) { 104 | called = true 105 | }) 106 | 107 | // Let's send a good request, it should pass 108 | { 109 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50&id2=50") 110 | assert.Equal(t, http.StatusOK, rec.Code) 111 | assert.True(t, called, "Handler should have been called") 112 | called = false 113 | } 114 | 115 | // Let's send a request with a missing parameter, it should return 116 | // a bad status 117 | { 118 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50") 119 | assert.Equal(t, http.StatusBadRequest, rec.Code) 120 | body, err := io.ReadAll(rec.Body) 121 | if assert.NoError(t, err) { 122 | assert.Contains(t, string(body), "parameter \"id2\"") 123 | assert.Contains(t, string(body), "value is required but missing") 124 | } 125 | assert.False(t, called, "Handler should not have been called") 126 | called = false 127 | } 128 | 129 | // Let's send a request with a 2 missing parameters, it should return 130 | // a bad status 131 | { 132 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource") 133 | assert.Equal(t, http.StatusBadRequest, rec.Code) 134 | body, err := io.ReadAll(rec.Body) 135 | if assert.NoError(t, err) { 136 | assert.Contains(t, string(body), "parameter \"id\"") 137 | assert.Contains(t, string(body), "value is required but missing") 138 | assert.Contains(t, string(body), "parameter \"id2\"") 139 | assert.Contains(t, string(body), "value is required but missing") 140 | } 141 | assert.False(t, called, "Handler should not have been called") 142 | called = false 143 | } 144 | 145 | // Let's send a request with a 1 missing parameter, and another outside 146 | // or the parameters. It should return a bad status 147 | { 148 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=500") 149 | assert.Equal(t, http.StatusBadRequest, rec.Code) 150 | body, err := io.ReadAll(rec.Body) 151 | if assert.NoError(t, err) { 152 | assert.Contains(t, string(body), "parameter \"id\"") 153 | assert.Contains(t, string(body), "number must be at most 100") 154 | assert.Contains(t, string(body), "parameter \"id2\"") 155 | assert.Contains(t, string(body), "value is required but missing") 156 | } 157 | assert.False(t, called, "Handler should not have been called") 158 | called = false 159 | } 160 | 161 | // Let's send a request with a parameters that do not meet spec. It should 162 | // return a bad status 163 | { 164 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=abc&id2=1") 165 | assert.Equal(t, http.StatusBadRequest, rec.Code) 166 | body, err := io.ReadAll(rec.Body) 167 | if assert.NoError(t, err) { 168 | assert.Contains(t, string(body), "parameter \"id\"") 169 | assert.Contains(t, string(body), "value abc: an invalid integer: invalid syntax") 170 | assert.Contains(t, string(body), "parameter \"id2\"") 171 | assert.Contains(t, string(body), "number must be at least 10") 172 | } 173 | assert.False(t, called, "Handler should not have been called") 174 | called = false 175 | } 176 | } 177 | 178 | func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) { 179 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 180 | require.NoError(t, err, "Error initializing OpenAPI spec") 181 | 182 | r := chi.NewRouter() 183 | 184 | // Set up an authenticator to check authenticated function. It will allow 185 | // access to "someScope", but disallow others. 186 | options := middleware.Options{ 187 | Options: openapi3filter.Options{ 188 | ExcludeRequestBody: false, 189 | ExcludeResponseBody: false, 190 | IncludeResponseStatus: true, 191 | MultiError: true, 192 | }, 193 | MultiErrorHandler: func(me openapi3.MultiError) (int, error) { 194 | return http.StatusTeapot, me 195 | }, 196 | } 197 | 198 | // register middleware 199 | r.Use(middleware.OapiRequestValidatorWithOptions(spec, &options)) 200 | 201 | called := false 202 | 203 | // Install a request handler for /resource. We want to make sure it doesn't 204 | // get called. 205 | r.Get("/multiparamresource", func(w http.ResponseWriter, r *http.Request) { 206 | called = true 207 | }) 208 | 209 | // Let's send a good request, it should pass 210 | { 211 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50&id2=50") 212 | assert.Equal(t, http.StatusOK, rec.Code) 213 | assert.True(t, called, "Handler should have been called") 214 | called = false 215 | } 216 | 217 | // Let's send a request with a missing parameter, it should return 218 | // a bad status 219 | { 220 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50") 221 | assert.Equal(t, http.StatusTeapot, rec.Code) 222 | body, err := io.ReadAll(rec.Body) 223 | if assert.NoError(t, err) { 224 | assert.Contains(t, string(body), "parameter \"id2\"") 225 | assert.Contains(t, string(body), "value is required but missing") 226 | } 227 | assert.False(t, called, "Handler should not have been called") 228 | called = false 229 | } 230 | 231 | // Let's send a request with a 2 missing parameters, it should return 232 | // a bad status 233 | { 234 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource") 235 | assert.Equal(t, http.StatusTeapot, rec.Code) 236 | body, err := io.ReadAll(rec.Body) 237 | if assert.NoError(t, err) { 238 | assert.Contains(t, string(body), "parameter \"id\"") 239 | assert.Contains(t, string(body), "value is required but missing") 240 | assert.Contains(t, string(body), "parameter \"id2\"") 241 | assert.Contains(t, string(body), "value is required but missing") 242 | } 243 | assert.False(t, called, "Handler should not have been called") 244 | called = false 245 | } 246 | 247 | // Let's send a request with a 1 missing parameter, and another outside 248 | // or the parameters. It should return a bad status 249 | { 250 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=500") 251 | assert.Equal(t, http.StatusTeapot, rec.Code) 252 | body, err := io.ReadAll(rec.Body) 253 | if assert.NoError(t, err) { 254 | assert.Contains(t, string(body), "parameter \"id\"") 255 | assert.Contains(t, string(body), "number must be at most 100") 256 | assert.Contains(t, string(body), "parameter \"id2\"") 257 | assert.Contains(t, string(body), "value is required but missing") 258 | } 259 | assert.False(t, called, "Handler should not have been called") 260 | called = false 261 | } 262 | 263 | // Let's send a request with a parameters that do not meet spec. It should 264 | // return a bad status 265 | { 266 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=abc&id2=1") 267 | assert.Equal(t, http.StatusTeapot, rec.Code) 268 | body, err := io.ReadAll(rec.Body) 269 | if assert.NoError(t, err) { 270 | assert.Contains(t, string(body), "parameter \"id\"") 271 | assert.Contains(t, string(body), "value abc: an invalid integer: invalid syntax") 272 | assert.Contains(t, string(body), "parameter \"id2\"") 273 | assert.Contains(t, string(body), "number must be at least 10") 274 | } 275 | assert.False(t, called, "Handler should not have been called") 276 | called = false 277 | } 278 | } 279 | 280 | func TestOapiRequestValidatorWithOptions(t *testing.T) { 281 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 282 | require.NoError(t, err, "Error initializing OpenAPI spec") 283 | 284 | r := chi.NewRouter() 285 | 286 | // Set up an authenticator to check authenticated function. It will allow 287 | // access to "someScope", but disallow others. 288 | options := middleware.Options{ 289 | ErrorHandler: func(w http.ResponseWriter, message string, statusCode int) { 290 | http.Error(w, "test: "+message, statusCode) 291 | }, 292 | Options: openapi3filter.Options{ 293 | AuthenticationFunc: func(c context.Context, input *openapi3filter.AuthenticationInput) error { 294 | 295 | for _, s := range input.Scopes { 296 | if s == "someScope" { 297 | return nil 298 | } 299 | } 300 | return errors.New("unauthorized") 301 | }, 302 | }, 303 | } 304 | 305 | // register middleware 306 | r.Use(middleware.OapiRequestValidatorWithOptions(spec, &options)) 307 | 308 | // basic cases 309 | testRequestValidatorBasicFunctions(t, r) 310 | 311 | called := false 312 | 313 | r.Get("/protected_resource", func(w http.ResponseWriter, r *http.Request) { 314 | called = true 315 | w.WriteHeader(http.StatusNoContent) 316 | }) 317 | 318 | // Call a protected function to which we have access 319 | { 320 | rec := doGet(t, r, "http://deepmap.ai/protected_resource") 321 | assert.Equal(t, http.StatusNoContent, rec.Code) 322 | assert.True(t, called, "Handler should have been called") 323 | called = false 324 | } 325 | 326 | r.Get("/protected_resource2", func(w http.ResponseWriter, r *http.Request) { 327 | called = true 328 | w.WriteHeader(http.StatusNoContent) 329 | }) 330 | // Call a protected function to which we dont have access 331 | { 332 | rec := doGet(t, r, "http://deepmap.ai/protected_resource2") 333 | assert.Equal(t, http.StatusUnauthorized, rec.Code) 334 | assert.False(t, called, "Handler should not have been called") 335 | called = false 336 | } 337 | 338 | r.Get("/protected_resource_401", func(w http.ResponseWriter, r *http.Request) { 339 | called = true 340 | w.WriteHeader(http.StatusNoContent) 341 | }) 342 | // Call a protected function without credentials 343 | { 344 | rec := doGet(t, r, "http://deepmap.ai/protected_resource_401") 345 | assert.Equal(t, http.StatusUnauthorized, rec.Code) 346 | assert.Equal(t, "test: security requirements failed: unauthorized\n", rec.Body.String()) 347 | assert.False(t, called, "Handler should not have been called") 348 | called = false 349 | } 350 | 351 | } 352 | 353 | func testRequestValidatorBasicFunctions(t *testing.T, r *chi.Mux) { 354 | 355 | called := false 356 | 357 | // Install a request handler for /resource. We want to make sure it doesn't 358 | // get called. 359 | r.Get("/resource", func(w http.ResponseWriter, r *http.Request) { 360 | called = true 361 | }) 362 | 363 | // Let's send the request to the wrong server, this should return 404 364 | { 365 | rec := doGet(t, r, "http://not.deepmap.ai/resource") 366 | assert.Equal(t, http.StatusNotFound, rec.Code) 367 | assert.False(t, called, "Handler should not have been called") 368 | } 369 | 370 | // Let's send a good request, it should pass 371 | { 372 | rec := doGet(t, r, "http://deepmap.ai/resource") 373 | assert.Equal(t, http.StatusOK, rec.Code) 374 | assert.True(t, called, "Handler should have been called") 375 | called = false 376 | } 377 | 378 | // Send an out-of-spec parameter 379 | { 380 | rec := doGet(t, r, "http://deepmap.ai/resource?id=500") 381 | assert.Equal(t, http.StatusBadRequest, rec.Code) 382 | assert.False(t, called, "Handler should not have been called") 383 | called = false 384 | } 385 | 386 | // Send a bad parameter type 387 | { 388 | rec := doGet(t, r, "http://deepmap.ai/resource?id=foo") 389 | assert.Equal(t, http.StatusBadRequest, rec.Code) 390 | assert.False(t, called, "Handler should not have been called") 391 | called = false 392 | } 393 | 394 | // Send a request with wrong HTTP method 395 | { 396 | body := struct { 397 | Name string `json:"name"` 398 | }{ 399 | Name: "Marcin", 400 | } 401 | rec := doPost(t, r, "http://deepmap.ai/resource", body) 402 | assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) 403 | assert.False(t, called, "Handler should not have been called") 404 | called = false 405 | } 406 | 407 | // Add a handler for the POST message 408 | r.Post("/resource", func(w http.ResponseWriter, r *http.Request) { 409 | called = true 410 | w.WriteHeader(http.StatusNoContent) 411 | }) 412 | 413 | called = false 414 | // Send a good request body 415 | { 416 | body := struct { 417 | Name string `json:"name"` 418 | }{ 419 | Name: "Marcin", 420 | } 421 | rec := doPost(t, r, "http://deepmap.ai/resource", body) 422 | assert.Equal(t, http.StatusNoContent, rec.Code) 423 | assert.True(t, called, "Handler should have been called") 424 | called = false 425 | } 426 | 427 | // Send a malformed body 428 | { 429 | body := struct { 430 | Name int `json:"name"` 431 | }{ 432 | Name: 7, 433 | } 434 | rec := doPost(t, r, "http://deepmap.ai/resource", body) 435 | assert.Equal(t, http.StatusBadRequest, rec.Code) 436 | assert.False(t, called, "Handler should not have been called") 437 | called = false 438 | } 439 | 440 | } 441 | -------------------------------------------------------------------------------- /internal/test/gorilla/oapi_validate_test.go: -------------------------------------------------------------------------------- 1 | package gorilla 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "testing" 14 | 15 | middleware "github.com/oapi-codegen/nethttp-middleware" 16 | 17 | "github.com/getkin/kin-openapi/openapi3" 18 | "github.com/getkin/kin-openapi/openapi3filter" 19 | "github.com/gorilla/mux" 20 | "github.com/stretchr/testify/assert" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | //go:embed test_spec.yaml 25 | var testSchema []byte 26 | 27 | func doGet(t *testing.T, mux http.Handler, rawURL string) *httptest.ResponseRecorder { 28 | u, err := url.Parse(rawURL) 29 | if err != nil { 30 | t.Fatalf("Invalid url: %s", rawURL) 31 | } 32 | 33 | req, err := http.NewRequest(http.MethodGet, u.String(), nil) 34 | require.NoError(t, err) 35 | 36 | req.Header.Set("accept", "application/json") 37 | 38 | rr := httptest.NewRecorder() 39 | 40 | mux.ServeHTTP(rr, req) 41 | 42 | return rr 43 | } 44 | 45 | func doPost(t *testing.T, mux http.Handler, rawURL string, jsonBody interface{}) *httptest.ResponseRecorder { 46 | u, err := url.Parse(rawURL) 47 | if err != nil { 48 | t.Fatalf("Invalid url: %s", rawURL) 49 | } 50 | 51 | data, err := json.Marshal(jsonBody) 52 | require.NoError(t, err) 53 | 54 | req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(data)) 55 | require.NoError(t, err) 56 | 57 | req.Header.Set("content-type", "application/json") 58 | 59 | rr := httptest.NewRecorder() 60 | 61 | mux.ServeHTTP(rr, req) 62 | 63 | return rr 64 | } 65 | 66 | func TestOapiRequestValidator(t *testing.T) { 67 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 68 | require.NoError(t, err, "Error initializing OpenAPI spec") 69 | 70 | r := mux.NewRouter() 71 | 72 | // register middleware 73 | r.Use(middleware.OapiRequestValidator(spec)) 74 | 75 | // basic cases 76 | testRequestValidatorBasicFunctions(t, r) 77 | } 78 | 79 | func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) { 80 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 81 | require.NoError(t, err, "Error initializing OpenAPI spec") 82 | 83 | r := mux.NewRouter() 84 | 85 | // Set up an authenticator to check authenticated function. It will allow 86 | // access to "someScope", but disallow others. 87 | options := middleware.Options{ 88 | Options: openapi3filter.Options{ 89 | ExcludeRequestBody: false, 90 | ExcludeResponseBody: false, 91 | IncludeResponseStatus: true, 92 | MultiError: true, 93 | }, 94 | } 95 | 96 | // register middleware 97 | r.Use(middleware.OapiRequestValidatorWithOptions(spec, &options)) 98 | 99 | called := false 100 | 101 | // Install a request handler for /resource. We want to make sure it doesn't 102 | // get called. 103 | r.HandleFunc("/multiparamresource", func(w http.ResponseWriter, r *http.Request) { 104 | called = true 105 | }).Methods("GET") 106 | 107 | // Let's send a good request, it should pass 108 | { 109 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50&id2=50") 110 | assert.Equal(t, http.StatusOK, rec.Code) 111 | assert.True(t, called, "Handler should have been called") 112 | called = false 113 | } 114 | 115 | // Let's send a request with a missing parameter, it should return 116 | // a bad status 117 | { 118 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50") 119 | assert.Equal(t, http.StatusBadRequest, rec.Code) 120 | body, err := io.ReadAll(rec.Body) 121 | if assert.NoError(t, err) { 122 | assert.Contains(t, string(body), "parameter \"id2\"") 123 | assert.Contains(t, string(body), "value is required but missing") 124 | } 125 | assert.False(t, called, "Handler should not have been called") 126 | called = false 127 | } 128 | 129 | // Let's send a request with a 2 missing parameters, it should return 130 | // a bad status 131 | { 132 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource") 133 | assert.Equal(t, http.StatusBadRequest, rec.Code) 134 | body, err := io.ReadAll(rec.Body) 135 | if assert.NoError(t, err) { 136 | assert.Contains(t, string(body), "parameter \"id\"") 137 | assert.Contains(t, string(body), "value is required but missing") 138 | assert.Contains(t, string(body), "parameter \"id2\"") 139 | assert.Contains(t, string(body), "value is required but missing") 140 | } 141 | assert.False(t, called, "Handler should not have been called") 142 | called = false 143 | } 144 | 145 | // Let's send a request with a 1 missing parameter, and another outside 146 | // or the parameters. It should return a bad status 147 | { 148 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=500") 149 | assert.Equal(t, http.StatusBadRequest, rec.Code) 150 | body, err := io.ReadAll(rec.Body) 151 | if assert.NoError(t, err) { 152 | assert.Contains(t, string(body), "parameter \"id\"") 153 | assert.Contains(t, string(body), "number must be at most 100") 154 | assert.Contains(t, string(body), "parameter \"id2\"") 155 | assert.Contains(t, string(body), "value is required but missing") 156 | } 157 | assert.False(t, called, "Handler should not have been called") 158 | called = false 159 | } 160 | 161 | // Let's send a request with a parameters that do not meet spec. It should 162 | // return a bad status 163 | { 164 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=abc&id2=1") 165 | assert.Equal(t, http.StatusBadRequest, rec.Code) 166 | body, err := io.ReadAll(rec.Body) 167 | if assert.NoError(t, err) { 168 | assert.Contains(t, string(body), "parameter \"id\"") 169 | assert.Contains(t, string(body), "value abc: an invalid integer: invalid syntax") 170 | assert.Contains(t, string(body), "parameter \"id2\"") 171 | assert.Contains(t, string(body), "number must be at least 10") 172 | } 173 | assert.False(t, called, "Handler should not have been called") 174 | called = false 175 | } 176 | } 177 | 178 | func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) { 179 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 180 | require.NoError(t, err, "Error initializing OpenAPI spec") 181 | 182 | r := mux.NewRouter() 183 | 184 | // Set up an authenticator to check authenticated function. It will allow 185 | // access to "someScope", but disallow others. 186 | options := middleware.Options{ 187 | Options: openapi3filter.Options{ 188 | ExcludeRequestBody: false, 189 | ExcludeResponseBody: false, 190 | IncludeResponseStatus: true, 191 | MultiError: true, 192 | }, 193 | MultiErrorHandler: func(me openapi3.MultiError) (int, error) { 194 | return http.StatusTeapot, me 195 | }, 196 | } 197 | 198 | // register middleware 199 | r.Use(middleware.OapiRequestValidatorWithOptions(spec, &options)) 200 | 201 | called := false 202 | 203 | // Install a request handler for /resource. We want to make sure it doesn't 204 | // get called. 205 | r.HandleFunc("/multiparamresource", func(w http.ResponseWriter, r *http.Request) { 206 | called = true 207 | }).Methods("GET") 208 | 209 | // Let's send a good request, it should pass 210 | { 211 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50&id2=50") 212 | assert.Equal(t, http.StatusOK, rec.Code) 213 | assert.True(t, called, "Handler should have been called") 214 | called = false 215 | } 216 | 217 | // Let's send a request with a missing parameter, it should return 218 | // a bad status 219 | { 220 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50") 221 | assert.Equal(t, http.StatusTeapot, rec.Code) 222 | body, err := io.ReadAll(rec.Body) 223 | if assert.NoError(t, err) { 224 | assert.Contains(t, string(body), "parameter \"id2\"") 225 | assert.Contains(t, string(body), "value is required but missing") 226 | } 227 | assert.False(t, called, "Handler should not have been called") 228 | called = false 229 | } 230 | 231 | // Let's send a request with a 2 missing parameters, it should return 232 | // a bad status 233 | { 234 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource") 235 | assert.Equal(t, http.StatusTeapot, rec.Code) 236 | body, err := io.ReadAll(rec.Body) 237 | if assert.NoError(t, err) { 238 | assert.Contains(t, string(body), "parameter \"id\"") 239 | assert.Contains(t, string(body), "value is required but missing") 240 | assert.Contains(t, string(body), "parameter \"id2\"") 241 | assert.Contains(t, string(body), "value is required but missing") 242 | } 243 | assert.False(t, called, "Handler should not have been called") 244 | called = false 245 | } 246 | 247 | // Let's send a request with a 1 missing parameter, and another outside 248 | // or the parameters. It should return a bad status 249 | { 250 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=500") 251 | assert.Equal(t, http.StatusTeapot, rec.Code) 252 | body, err := io.ReadAll(rec.Body) 253 | if assert.NoError(t, err) { 254 | assert.Contains(t, string(body), "parameter \"id\"") 255 | assert.Contains(t, string(body), "number must be at most 100") 256 | assert.Contains(t, string(body), "parameter \"id2\"") 257 | assert.Contains(t, string(body), "value is required but missing") 258 | } 259 | assert.False(t, called, "Handler should not have been called") 260 | called = false 261 | } 262 | 263 | // Let's send a request with a parameters that do not meet spec. It should 264 | // return a bad status 265 | { 266 | rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=abc&id2=1") 267 | assert.Equal(t, http.StatusTeapot, rec.Code) 268 | body, err := io.ReadAll(rec.Body) 269 | if assert.NoError(t, err) { 270 | assert.Contains(t, string(body), "parameter \"id\"") 271 | assert.Contains(t, string(body), "value abc: an invalid integer: invalid syntax") 272 | assert.Contains(t, string(body), "parameter \"id2\"") 273 | assert.Contains(t, string(body), "number must be at least 10") 274 | } 275 | assert.False(t, called, "Handler should not have been called") 276 | called = false 277 | } 278 | } 279 | 280 | func TestOapiRequestValidatorWithOptions(t *testing.T) { 281 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 282 | require.NoError(t, err, "Error initializing OpenAPI spec") 283 | 284 | r := mux.NewRouter() 285 | 286 | // Set up an authenticator to check authenticated function. It will allow 287 | // access to "someScope", but disallow others. 288 | options := middleware.Options{ 289 | ErrorHandler: func(w http.ResponseWriter, message string, statusCode int) { 290 | http.Error(w, "test: "+message, statusCode) 291 | }, 292 | Options: openapi3filter.Options{ 293 | AuthenticationFunc: func(c context.Context, input *openapi3filter.AuthenticationInput) error { 294 | 295 | for _, s := range input.Scopes { 296 | if s == "someScope" { 297 | return nil 298 | } 299 | } 300 | return errors.New("unauthorized") 301 | }, 302 | }, 303 | } 304 | 305 | // register middleware 306 | r.Use(middleware.OapiRequestValidatorWithOptions(spec, &options)) 307 | 308 | // basic cases 309 | testRequestValidatorBasicFunctions(t, r) 310 | 311 | called := false 312 | 313 | r.HandleFunc("/protected_resource", func(w http.ResponseWriter, r *http.Request) { 314 | called = true 315 | w.WriteHeader(http.StatusNoContent) 316 | }).Methods("GET") 317 | 318 | // Call a protected function to which we have access 319 | { 320 | rec := doGet(t, r, "http://deepmap.ai/protected_resource") 321 | assert.Equal(t, http.StatusNoContent, rec.Code) 322 | assert.True(t, called, "Handler should have been called") 323 | called = false 324 | } 325 | 326 | r.HandleFunc("/protected_resource2", func(w http.ResponseWriter, r *http.Request) { 327 | called = true 328 | w.WriteHeader(http.StatusNoContent) 329 | }).Methods("GET") 330 | // Call a protected function to which we dont have access 331 | { 332 | rec := doGet(t, r, "http://deepmap.ai/protected_resource2") 333 | assert.Equal(t, http.StatusUnauthorized, rec.Code) 334 | assert.False(t, called, "Handler should not have been called") 335 | called = false 336 | } 337 | 338 | r.HandleFunc("/protected_resource_401", func(w http.ResponseWriter, r *http.Request) { 339 | called = true 340 | w.WriteHeader(http.StatusNoContent) 341 | }).Methods("GET") 342 | // Call a protected function without credentials 343 | { 344 | rec := doGet(t, r, "http://deepmap.ai/protected_resource_401") 345 | assert.Equal(t, http.StatusUnauthorized, rec.Code) 346 | assert.Equal(t, "test: security requirements failed: unauthorized\n", rec.Body.String()) 347 | assert.False(t, called, "Handler should not have been called") 348 | called = false 349 | } 350 | 351 | } 352 | 353 | func testRequestValidatorBasicFunctions(t *testing.T, r *mux.Router) { 354 | 355 | called := false 356 | 357 | // Install a request handler for /resource. We want to make sure it doesn't 358 | // get called. 359 | r.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 360 | called = true 361 | }).Methods("GET") 362 | 363 | // Let's send the request to the wrong server, this should return 404 364 | { 365 | rec := doGet(t, r, "http://not.deepmap.ai/resource") 366 | assert.Equal(t, http.StatusNotFound, rec.Code) 367 | assert.False(t, called, "Handler should not have been called") 368 | } 369 | 370 | // Let's send a good request, it should pass 371 | { 372 | rec := doGet(t, r, "http://deepmap.ai/resource") 373 | assert.Equal(t, http.StatusOK, rec.Code) 374 | assert.True(t, called, "Handler should have been called") 375 | called = false 376 | } 377 | 378 | // Send an out-of-spec parameter 379 | { 380 | rec := doGet(t, r, "http://deepmap.ai/resource?id=500") 381 | assert.Equal(t, http.StatusBadRequest, rec.Code) 382 | assert.False(t, called, "Handler should not have been called") 383 | called = false 384 | } 385 | 386 | // Send a bad parameter type 387 | { 388 | rec := doGet(t, r, "http://deepmap.ai/resource?id=foo") 389 | assert.Equal(t, http.StatusBadRequest, rec.Code) 390 | assert.False(t, called, "Handler should not have been called") 391 | called = false 392 | } 393 | 394 | // Send a request with wrong HTTP method 395 | { 396 | body := struct { 397 | Name string `json:"name"` 398 | }{ 399 | Name: "Marcin", 400 | } 401 | rec := doPost(t, r, "http://deepmap.ai/resource", body) 402 | assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) 403 | assert.False(t, called, "Handler should not have been called") 404 | called = false 405 | } 406 | 407 | // Add a handler for the POST message 408 | r.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 409 | called = true 410 | w.WriteHeader(http.StatusNoContent) 411 | }).Methods("POST") 412 | 413 | called = false 414 | // Send a good request body 415 | { 416 | body := struct { 417 | Name string `json:"name"` 418 | }{ 419 | Name: "Marcin", 420 | } 421 | rec := doPost(t, r, "http://deepmap.ai/resource", body) 422 | assert.Equal(t, http.StatusNoContent, rec.Code) 423 | assert.True(t, called, "Handler should have been called") 424 | called = false 425 | } 426 | 427 | // Send a malformed body 428 | { 429 | body := struct { 430 | Name int `json:"name"` 431 | }{ 432 | Name: 7, 433 | } 434 | rec := doPost(t, r, "http://deepmap.ai/resource", body) 435 | assert.Equal(t, http.StatusBadRequest, rec.Code) 436 | assert.False(t, called, "Handler should not have been called") 437 | called = false 438 | } 439 | 440 | } 441 | -------------------------------------------------------------------------------- /internal/test/nethttp/oapi_validate_test.go: -------------------------------------------------------------------------------- 1 | package gorilla 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "testing" 14 | 15 | middleware "github.com/oapi-codegen/nethttp-middleware" 16 | 17 | "github.com/getkin/kin-openapi/openapi3" 18 | "github.com/getkin/kin-openapi/openapi3filter" 19 | "github.com/stretchr/testify/assert" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | //go:embed test_spec.yaml 24 | var testSchema []byte 25 | 26 | func doGet(t *testing.T, mux http.Handler, rawURL string) *httptest.ResponseRecorder { 27 | u, err := url.Parse(rawURL) 28 | if err != nil { 29 | t.Fatalf("Invalid url: %s", rawURL) 30 | } 31 | 32 | req, err := http.NewRequest(http.MethodGet, u.String(), nil) 33 | require.NoError(t, err) 34 | 35 | req.Header.Set("accept", "application/json") 36 | 37 | rr := httptest.NewRecorder() 38 | 39 | mux.ServeHTTP(rr, req) 40 | 41 | return rr 42 | } 43 | 44 | func doPost(t *testing.T, mux http.Handler, rawURL string, jsonBody interface{}) *httptest.ResponseRecorder { 45 | u, err := url.Parse(rawURL) 46 | if err != nil { 47 | t.Fatalf("Invalid url: %s", rawURL) 48 | } 49 | 50 | data, err := json.Marshal(jsonBody) 51 | require.NoError(t, err) 52 | 53 | req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(data)) 54 | require.NoError(t, err) 55 | 56 | req.Header.Set("content-type", "application/json") 57 | 58 | rr := httptest.NewRecorder() 59 | 60 | mux.ServeHTTP(rr, req) 61 | 62 | return rr 63 | } 64 | 65 | func doPatch(t *testing.T, mux http.Handler, rawURL string, jsonBody interface{}) *httptest.ResponseRecorder { 66 | u, err := url.Parse(rawURL) 67 | if err != nil { 68 | t.Fatalf("Invalid url: %s", rawURL) 69 | } 70 | 71 | data, err := json.Marshal(jsonBody) 72 | require.NoError(t, err) 73 | 74 | req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(data)) 75 | require.NoError(t, err) 76 | 77 | req.Header.Set("content-type", "application/json") 78 | 79 | rr := httptest.NewRecorder() 80 | 81 | mux.ServeHTTP(rr, req) 82 | 83 | return rr 84 | } 85 | 86 | // use wraps a given http.ServeMux with middleware for execution 87 | func use(r *http.ServeMux, mw func(next http.Handler) http.Handler) http.Handler { 88 | return mw(r) 89 | } 90 | 91 | func TestOapiRequestValidator(t *testing.T) { 92 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 93 | require.NoError(t, err, "Error initializing OpenAPI spec") 94 | 95 | r := http.NewServeMux() 96 | 97 | // create middleware 98 | mw := middleware.OapiRequestValidator(spec) 99 | 100 | // basic cases 101 | testRequestValidatorBasicFunctions(t, r, mw) 102 | } 103 | 104 | func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) { 105 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 106 | require.NoError(t, err, "Error initializing OpenAPI spec") 107 | 108 | // Set up an authenticator to check authenticated function. It will allow 109 | // access to "someScope", but disallow others. 110 | options := middleware.Options{ 111 | Options: openapi3filter.Options{ 112 | ExcludeRequestBody: false, 113 | ExcludeResponseBody: false, 114 | IncludeResponseStatus: true, 115 | MultiError: true, 116 | }, 117 | } 118 | 119 | r := http.NewServeMux() 120 | 121 | called := false 122 | 123 | // Install a request handler for /resource. We want to make sure it doesn't 124 | // get called. 125 | r.HandleFunc("/multiparamresource", func(w http.ResponseWriter, r *http.Request) { 126 | if r.Method != http.MethodGet { 127 | w.WriteHeader(http.StatusMethodNotAllowed) 128 | return 129 | } 130 | 131 | called = true 132 | }) 133 | 134 | // register middleware 135 | mw := middleware.OapiRequestValidatorWithOptions(spec, &options) 136 | server := mw(r) 137 | 138 | // Let's send a good request, it should pass 139 | { 140 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=50&id2=50") 141 | assert.Equal(t, http.StatusOK, rec.Code) 142 | assert.True(t, called, "Handler should have been called") 143 | called = false 144 | } 145 | 146 | // Let's send a request with a missing parameter, it should return 147 | // a bad status 148 | { 149 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=50") 150 | assert.Equal(t, http.StatusBadRequest, rec.Code) 151 | body, err := io.ReadAll(rec.Body) 152 | if assert.NoError(t, err) { 153 | assert.Contains(t, string(body), "parameter \"id2\"") 154 | assert.Contains(t, string(body), "value is required but missing") 155 | } 156 | assert.False(t, called, "Handler should not have been called") 157 | called = false 158 | } 159 | 160 | // Let's send a request with a 2 missing parameters, it should return 161 | // a bad status 162 | { 163 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource") 164 | assert.Equal(t, http.StatusBadRequest, rec.Code) 165 | body, err := io.ReadAll(rec.Body) 166 | if assert.NoError(t, err) { 167 | assert.Contains(t, string(body), "parameter \"id\"") 168 | assert.Contains(t, string(body), "value is required but missing") 169 | assert.Contains(t, string(body), "parameter \"id2\"") 170 | assert.Contains(t, string(body), "value is required but missing") 171 | } 172 | assert.False(t, called, "Handler should not have been called") 173 | called = false 174 | } 175 | 176 | // Let's send a request with a 1 missing parameter, and another outside 177 | // or the parameters. It should return a bad status 178 | { 179 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=500") 180 | assert.Equal(t, http.StatusBadRequest, rec.Code) 181 | body, err := io.ReadAll(rec.Body) 182 | if assert.NoError(t, err) { 183 | assert.Contains(t, string(body), "parameter \"id\"") 184 | assert.Contains(t, string(body), "number must be at most 100") 185 | assert.Contains(t, string(body), "parameter \"id2\"") 186 | assert.Contains(t, string(body), "value is required but missing") 187 | } 188 | assert.False(t, called, "Handler should not have been called") 189 | called = false 190 | } 191 | 192 | // Let's send a request with a parameters that do not meet spec. It should 193 | // return a bad status 194 | { 195 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=abc&id2=1") 196 | assert.Equal(t, http.StatusBadRequest, rec.Code) 197 | body, err := io.ReadAll(rec.Body) 198 | if assert.NoError(t, err) { 199 | assert.Contains(t, string(body), "parameter \"id\"") 200 | assert.Contains(t, string(body), "value abc: an invalid integer: invalid syntax") 201 | assert.Contains(t, string(body), "parameter \"id2\"") 202 | assert.Contains(t, string(body), "number must be at least 10") 203 | } 204 | assert.False(t, called, "Handler should not have been called") 205 | called = false 206 | } 207 | } 208 | 209 | func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) { 210 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 211 | require.NoError(t, err, "Error initializing OpenAPI spec") 212 | 213 | r := http.NewServeMux() 214 | 215 | // Set up an authenticator to check authenticated function. It will allow 216 | // access to "someScope", but disallow others. 217 | options := middleware.Options{ 218 | Options: openapi3filter.Options{ 219 | ExcludeRequestBody: false, 220 | ExcludeResponseBody: false, 221 | IncludeResponseStatus: true, 222 | MultiError: true, 223 | }, 224 | MultiErrorHandler: func(me openapi3.MultiError) (int, error) { 225 | return http.StatusTeapot, me 226 | }, 227 | } 228 | 229 | called := false 230 | 231 | // Install a request handler for /resource. We want to make sure it doesn't 232 | // get called. 233 | r.HandleFunc("/multiparamresource", func(w http.ResponseWriter, r *http.Request) { 234 | if r.Method != http.MethodGet { 235 | w.WriteHeader(http.StatusMethodNotAllowed) 236 | return 237 | } 238 | 239 | called = true 240 | }) 241 | 242 | // register middleware 243 | server := use(r, middleware.OapiRequestValidatorWithOptions(spec, &options)) 244 | 245 | // Let's send a good request, it should pass 246 | { 247 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=50&id2=50") 248 | assert.Equal(t, http.StatusOK, rec.Code) 249 | assert.True(t, called, "Handler should have been called") 250 | called = false 251 | } 252 | 253 | // Let's send a request with a missing parameter, it should return 254 | // a bad status 255 | { 256 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=50") 257 | assert.Equal(t, http.StatusTeapot, rec.Code) 258 | body, err := io.ReadAll(rec.Body) 259 | if assert.NoError(t, err) { 260 | assert.Contains(t, string(body), "parameter \"id2\"") 261 | assert.Contains(t, string(body), "value is required but missing") 262 | } 263 | assert.False(t, called, "Handler should not have been called") 264 | called = false 265 | } 266 | 267 | // Let's send a request with a 2 missing parameters, it should return 268 | // a bad status 269 | { 270 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource") 271 | assert.Equal(t, http.StatusTeapot, rec.Code) 272 | body, err := io.ReadAll(rec.Body) 273 | if assert.NoError(t, err) { 274 | assert.Contains(t, string(body), "parameter \"id\"") 275 | assert.Contains(t, string(body), "value is required but missing") 276 | assert.Contains(t, string(body), "parameter \"id2\"") 277 | assert.Contains(t, string(body), "value is required but missing") 278 | } 279 | assert.False(t, called, "Handler should not have been called") 280 | called = false 281 | } 282 | 283 | // Let's send a request with a 1 missing parameter, and another outside 284 | // or the parameters. It should return a bad status 285 | { 286 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=500") 287 | assert.Equal(t, http.StatusTeapot, rec.Code) 288 | body, err := io.ReadAll(rec.Body) 289 | if assert.NoError(t, err) { 290 | assert.Contains(t, string(body), "parameter \"id\"") 291 | assert.Contains(t, string(body), "number must be at most 100") 292 | assert.Contains(t, string(body), "parameter \"id2\"") 293 | assert.Contains(t, string(body), "value is required but missing") 294 | } 295 | assert.False(t, called, "Handler should not have been called") 296 | called = false 297 | } 298 | 299 | // Let's send a request with a parameters that do not meet spec. It should 300 | // return a bad status 301 | { 302 | rec := doGet(t, server, "http://deepmap.ai/multiparamresource?id=abc&id2=1") 303 | assert.Equal(t, http.StatusTeapot, rec.Code) 304 | body, err := io.ReadAll(rec.Body) 305 | if assert.NoError(t, err) { 306 | assert.Contains(t, string(body), "parameter \"id\"") 307 | assert.Contains(t, string(body), "value abc: an invalid integer: invalid syntax") 308 | assert.Contains(t, string(body), "parameter \"id2\"") 309 | assert.Contains(t, string(body), "number must be at least 10") 310 | } 311 | assert.False(t, called, "Handler should not have been called") 312 | called = false 313 | } 314 | } 315 | 316 | func TestOapiRequestValidatorWithOptions(t *testing.T) { 317 | spec, err := openapi3.NewLoader().LoadFromData(testSchema) 318 | require.NoError(t, err, "Error initializing OpenAPI spec") 319 | 320 | r := http.NewServeMux() 321 | 322 | // Set up an authenticator to check authenticated function. It will allow 323 | // access to "someScope", but disallow others. 324 | options := middleware.Options{ 325 | ErrorHandler: func(w http.ResponseWriter, message string, statusCode int) { 326 | http.Error(w, "test: "+message, statusCode) 327 | }, 328 | Options: openapi3filter.Options{ 329 | AuthenticationFunc: func(c context.Context, input *openapi3filter.AuthenticationInput) error { 330 | 331 | for _, s := range input.Scopes { 332 | if s == "someScope" { 333 | return nil 334 | } 335 | } 336 | return errors.New("unauthorized") 337 | }, 338 | }, 339 | } 340 | 341 | // register middleware 342 | mw := middleware.OapiRequestValidatorWithOptions(spec, &options) 343 | server := use(r, mw) 344 | 345 | // basic cases 346 | testRequestValidatorBasicFunctions(t, r, mw) 347 | 348 | called := false 349 | 350 | r.HandleFunc("/protected_resource", func(w http.ResponseWriter, r *http.Request) { 351 | if r.Method != http.MethodGet { 352 | w.WriteHeader(http.StatusMethodNotAllowed) 353 | return 354 | } 355 | 356 | called = true 357 | w.WriteHeader(http.StatusNoContent) 358 | }) 359 | 360 | // Call a protected function to which we have access 361 | { 362 | rec := doGet(t, server, "http://deepmap.ai/protected_resource") 363 | assert.Equal(t, http.StatusNoContent, rec.Code) 364 | assert.True(t, called, "Handler should have been called") 365 | called = false 366 | } 367 | 368 | r.HandleFunc("/protected_resource2", func(w http.ResponseWriter, r *http.Request) { 369 | if r.Method != http.MethodGet { 370 | w.WriteHeader(http.StatusMethodNotAllowed) 371 | return 372 | } 373 | 374 | called = true 375 | w.WriteHeader(http.StatusNoContent) 376 | }) 377 | // Call a protected function to which we dont have access 378 | { 379 | rec := doGet(t, server, "http://deepmap.ai/protected_resource2") 380 | assert.Equal(t, http.StatusUnauthorized, rec.Code) 381 | assert.False(t, called, "Handler should not have been called") 382 | called = false 383 | } 384 | 385 | r.HandleFunc("/protected_resource_401", func(w http.ResponseWriter, r *http.Request) { 386 | if r.Method != http.MethodGet { 387 | w.WriteHeader(http.StatusMethodNotAllowed) 388 | return 389 | } 390 | 391 | called = true 392 | w.WriteHeader(http.StatusNoContent) 393 | }) 394 | // Call a protected function without credentials 395 | { 396 | rec := doGet(t, server, "http://deepmap.ai/protected_resource_401") 397 | assert.Equal(t, http.StatusUnauthorized, rec.Code) 398 | assert.Equal(t, "test: security requirements failed: unauthorized\n", rec.Body.String()) 399 | assert.False(t, called, "Handler should not have been called") 400 | called = false 401 | } 402 | 403 | } 404 | 405 | func testRequestValidatorBasicFunctions(t *testing.T, r *http.ServeMux, mw func(next http.Handler) http.Handler) { 406 | called := false 407 | 408 | // Install a request handler for /resource. We want to make sure it doesn't 409 | // get called. 410 | r.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 411 | switch r.Method { 412 | case http.MethodGet: 413 | called = true 414 | w.WriteHeader(http.StatusOK) 415 | return 416 | case http.MethodPost: 417 | called = true 418 | w.WriteHeader(http.StatusNoContent) 419 | return 420 | } 421 | 422 | w.WriteHeader(http.StatusMethodNotAllowed) 423 | }) 424 | 425 | server := use(r, mw) 426 | 427 | // Let's send the request to the wrong server, this should return 404 428 | { 429 | rec := doGet(t, server, "http://not.deepmap.ai/resource") 430 | assert.Equal(t, http.StatusNotFound, rec.Code) 431 | assert.False(t, called, "Handler should not have been called") 432 | } 433 | 434 | // Let's send a good request, it should pass 435 | { 436 | rec := doGet(t, server, "http://deepmap.ai/resource") 437 | assert.Equal(t, http.StatusOK, rec.Code) 438 | assert.True(t, called, "Handler should have been called") 439 | called = false 440 | } 441 | 442 | // Send an out-of-spec parameter 443 | { 444 | rec := doGet(t, server, "http://deepmap.ai/resource?id=500") 445 | assert.Equal(t, http.StatusBadRequest, rec.Code) 446 | assert.False(t, called, "Handler should not have been called") 447 | called = false 448 | } 449 | 450 | // Send a bad parameter type 451 | { 452 | rec := doGet(t, server, "http://deepmap.ai/resource?id=foo") 453 | assert.Equal(t, http.StatusBadRequest, rec.Code) 454 | assert.False(t, called, "Handler should not have been called") 455 | called = false 456 | } 457 | 458 | // Send a request with wrong method 459 | { 460 | body := struct { 461 | Name string `json:"name"` 462 | }{ 463 | Name: "Marcin", 464 | } 465 | rec := doPatch(t, server, "http://deepmap.ai/resource", body) 466 | assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) 467 | assert.False(t, called, "Handler should not have been called") 468 | called = false 469 | } 470 | 471 | called = false 472 | // Send a good request body 473 | { 474 | body := struct { 475 | Name string `json:"name"` 476 | }{ 477 | Name: "Marcin", 478 | } 479 | rec := doPost(t, server, "http://deepmap.ai/resource", body) 480 | assert.Equal(t, http.StatusNoContent, rec.Code) 481 | assert.True(t, called, "Handler should have been called") 482 | called = false 483 | } 484 | 485 | // Send a malformed body 486 | { 487 | body := struct { 488 | Name int `json:"name"` 489 | }{ 490 | Name: 7, 491 | } 492 | rec := doPost(t, server, "http://deepmap.ai/resource", body) 493 | assert.Equal(t, http.StatusBadRequest, rec.Code) 494 | assert.False(t, called, "Handler should not have been called") 495 | called = false 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /oapi_validate_example_test.go: -------------------------------------------------------------------------------- 1 | package nethttpmiddleware_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "reflect" 12 | 13 | "github.com/getkin/kin-openapi/openapi3" 14 | "github.com/getkin/kin-openapi/openapi3filter" 15 | middleware "github.com/oapi-codegen/nethttp-middleware" 16 | ) 17 | 18 | func ExampleOapiRequestValidatorWithOptions() { 19 | rawSpec := ` 20 | openapi: "3.0.0" 21 | info: 22 | version: 1.0.0 23 | title: TestServer 24 | servers: 25 | - url: http://example.com/ 26 | paths: 27 | /resource: 28 | post: 29 | operationId: createResource 30 | responses: 31 | '204': 32 | description: No content 33 | requestBody: 34 | required: true 35 | content: 36 | application/json: 37 | schema: 38 | properties: 39 | name: 40 | type: string 41 | additionalProperties: false 42 | /protected_resource: 43 | get: 44 | operationId: getProtectedResource 45 | security: 46 | - BearerAuth: 47 | - someScope 48 | responses: 49 | '204': 50 | description: no content 51 | components: 52 | securitySchemes: 53 | BearerAuth: 54 | type: http 55 | scheme: bearer 56 | bearerFormat: JWT 57 | ` 58 | 59 | must := func(err error) { 60 | if err != nil { 61 | panic(err) 62 | } 63 | } 64 | 65 | use := func(r *http.ServeMux, middlewares ...func(next http.Handler) http.Handler) http.Handler { 66 | var s http.Handler 67 | s = r 68 | 69 | for _, mw := range middlewares { 70 | s = mw(s) 71 | } 72 | 73 | return s 74 | } 75 | 76 | logResponseBody := func(rr *httptest.ResponseRecorder) { 77 | if rr.Result().Body != nil { 78 | data, _ := io.ReadAll(rr.Result().Body) 79 | if len(data) > 0 { 80 | fmt.Printf("Response body: %s", data) 81 | } 82 | } 83 | } 84 | 85 | spec, err := openapi3.NewLoader().LoadFromData([]byte(rawSpec)) 86 | must(err) 87 | 88 | // 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 89 | // See also: Options#SilenceServersWarning 90 | spec.Servers = nil 91 | 92 | router := http.NewServeMux() 93 | 94 | router.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 95 | fmt.Printf("%s /resource was called\n", r.Method) 96 | 97 | if r.Method == http.MethodPost { 98 | w.WriteHeader(http.StatusNoContent) 99 | return 100 | } 101 | 102 | w.WriteHeader(http.StatusMethodNotAllowed) 103 | }) 104 | 105 | router.HandleFunc("/protected_resource", func(w http.ResponseWriter, r *http.Request) { 106 | // NOTE that we're setting up our `authenticationFunc` (below) to /never/ allow any requests in - so if we get a response from this endpoint, our `authenticationFunc` hasn't correctly worked 107 | 108 | if r.Method == http.MethodGet { 109 | w.WriteHeader(http.StatusNoContent) 110 | return 111 | } 112 | 113 | w.WriteHeader(http.StatusMethodNotAllowed) 114 | }) 115 | 116 | authenticationFunc := func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error { 117 | fmt.Printf("`AuthenticationFunc` was called for securitySchemeName=%s\n", ai.SecuritySchemeName) 118 | return fmt.Errorf("this check always fails - don't let anyone in!") 119 | } 120 | 121 | // create middleware 122 | mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{ 123 | Options: openapi3filter.Options{ 124 | AuthenticationFunc: authenticationFunc, 125 | }, 126 | }) 127 | 128 | // then wire it in 129 | server := use(router, mw) 130 | 131 | // ================================================================================ 132 | fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body)") 133 | 134 | req, err := http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(nil)) 135 | must(err) 136 | req.Header.Set("Content-Type", "application/json") 137 | 138 | rr := httptest.NewRecorder() 139 | 140 | server.ServeHTTP(rr, req) 141 | 142 | fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code) 143 | logResponseBody(rr) 144 | fmt.Println() 145 | 146 | // ================================================================================ 147 | fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (because an invalid property is sent, and we have `additionalProperties: false`)") 148 | body := map[string]string{ 149 | "invalid": "not expected", 150 | } 151 | 152 | data, err := json.Marshal(body) 153 | must(err) 154 | 155 | req, err = http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(data)) 156 | must(err) 157 | req.Header.Set("Content-Type", "application/json") 158 | 159 | rr = httptest.NewRecorder() 160 | 161 | server.ServeHTTP(rr, req) 162 | 163 | fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code) 164 | logResponseBody(rr) 165 | fmt.Println() 166 | 167 | // ================================================================================ 168 | fmt.Println("# A request with an invalid HTTP method, to a valid path, is rejected with an HTTP 405 Method Not Allowed") 169 | body = map[string]string{ 170 | "invalid": "not expected", 171 | } 172 | 173 | data, err = json.Marshal(body) 174 | must(err) 175 | 176 | req, err = http.NewRequest(http.MethodPatch, "/resource", bytes.NewReader(data)) 177 | must(err) 178 | req.Header.Set("Content-Type", "application/json") 179 | 180 | rr = httptest.NewRecorder() 181 | 182 | server.ServeHTTP(rr, req) 183 | 184 | fmt.Printf("Received an HTTP %d response. Expected HTTP 405\n", rr.Code) 185 | logResponseBody(rr) 186 | fmt.Println() 187 | 188 | // ================================================================================ 189 | fmt.Println("# A request that is well-formed is passed through to the Handler") 190 | body = map[string]string{ 191 | "name": "Jamie", 192 | } 193 | 194 | data, err = json.Marshal(body) 195 | must(err) 196 | 197 | req, err = http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(data)) 198 | must(err) 199 | req.Header.Set("Content-Type", "application/json") 200 | 201 | rr = httptest.NewRecorder() 202 | 203 | server.ServeHTTP(rr, req) 204 | 205 | fmt.Printf("Received an HTTP %d response. Expected HTTP 204\n", rr.Code) 206 | logResponseBody(rr) 207 | fmt.Println() 208 | 209 | // ================================================================================ 210 | fmt.Println("# A request to an authenticated endpoint must go through an `AuthenticationFunc`, and if it fails, an HTTP 401 is returned") 211 | 212 | req, err = http.NewRequest(http.MethodGet, "/protected_resource", nil) 213 | must(err) 214 | 215 | rr = httptest.NewRecorder() 216 | 217 | server.ServeHTTP(rr, req) 218 | 219 | fmt.Printf("Received an HTTP %d response. Expected HTTP 401\n", rr.Code) 220 | logResponseBody(rr) 221 | fmt.Println() 222 | 223 | // Output: 224 | // # A request that is malformed is rejected with HTTP 400 Bad Request (with no request body) 225 | // Received an HTTP 400 response. Expected HTTP 400 226 | // Response body: request body has an error: value is required but missing 227 | // 228 | // # A request that is malformed is rejected with HTTP 400 Bad Request (because an invalid property is sent, and we have `additionalProperties: false`) 229 | // Received an HTTP 400 response. Expected HTTP 400 230 | // Response body: request body has an error: doesn't match schema: property "invalid" is unsupported 231 | // 232 | // # A request with an invalid HTTP method, to a valid path, is rejected with an HTTP 405 Method Not Allowed 233 | // Received an HTTP 405 response. Expected HTTP 405 234 | // Response body: method not allowed 235 | // 236 | // # A request that is well-formed is passed through to the Handler 237 | // POST /resource was called 238 | // Received an HTTP 204 response. Expected HTTP 204 239 | // 240 | // # A request to an authenticated endpoint must go through an `AuthenticationFunc`, and if it fails, an HTTP 401 is returned 241 | // `AuthenticationFunc` was called for securitySchemeName=BearerAuth 242 | // Received an HTTP 401 response. Expected HTTP 401 243 | // Response body: security requirements failed: this check always fails - don't let anyone in! 244 | } 245 | 246 | func ExampleOapiRequestValidatorWithOptions_withErrorHandler() { 247 | rawSpec := ` 248 | openapi: "3.0.0" 249 | info: 250 | version: 1.0.0 251 | title: TestServer 252 | servers: 253 | - url: http://example.com/ 254 | paths: 255 | /resource: 256 | post: 257 | operationId: createResource 258 | responses: 259 | '204': 260 | description: No content 261 | requestBody: 262 | required: true 263 | content: 264 | application/json: 265 | schema: 266 | properties: 267 | name: 268 | type: string 269 | additionalProperties: false 270 | ` 271 | 272 | must := func(err error) { 273 | if err != nil { 274 | panic(err) 275 | } 276 | } 277 | 278 | use := func(r *http.ServeMux, middlewares ...func(next http.Handler) http.Handler) http.Handler { 279 | var s http.Handler 280 | s = r 281 | 282 | for _, mw := range middlewares { 283 | s = mw(s) 284 | } 285 | 286 | return s 287 | } 288 | 289 | logResponseBody := func(rr *httptest.ResponseRecorder) { 290 | if rr.Result().Body != nil { 291 | data, _ := io.ReadAll(rr.Result().Body) 292 | if len(data) > 0 { 293 | fmt.Printf("Response body: %s", data) 294 | } 295 | } 296 | } 297 | 298 | spec, err := openapi3.NewLoader().LoadFromData([]byte(rawSpec)) 299 | must(err) 300 | 301 | // 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 302 | // See also: Options#SilenceServersWarning 303 | spec.Servers = nil 304 | 305 | router := http.NewServeMux() 306 | 307 | router.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 308 | fmt.Printf("%s /resource was called\n", r.Method) 309 | 310 | if r.Method == http.MethodPost { 311 | w.WriteHeader(http.StatusNoContent) 312 | return 313 | } 314 | 315 | w.WriteHeader(http.StatusMethodNotAllowed) 316 | }) 317 | 318 | authenticationFunc := func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error { 319 | fmt.Printf("`AuthenticationFunc` was called for securitySchemeName=%s\n", ai.SecuritySchemeName) 320 | return fmt.Errorf("this check always fails - don't let anyone in!") 321 | } 322 | 323 | errorHandlerFunc := func(w http.ResponseWriter, message string, statusCode int) { 324 | fmt.Printf("ErrorHandler: An HTTP %d was returned by the middleware with error message: %s\n", statusCode, message) 325 | http.Error(w, "This was rewritten by the ErrorHandler", statusCode) 326 | } 327 | 328 | // create middleware 329 | mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{ 330 | Options: openapi3filter.Options{ 331 | AuthenticationFunc: authenticationFunc, 332 | }, 333 | ErrorHandler: errorHandlerFunc, 334 | }) 335 | 336 | // then wire it in 337 | server := use(router, mw) 338 | 339 | // ================================================================================ 340 | fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandler") 341 | 342 | req, err := http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(nil)) 343 | must(err) 344 | req.Header.Set("Content-Type", "application/json") 345 | 346 | rr := httptest.NewRecorder() 347 | 348 | server.ServeHTTP(rr, req) 349 | 350 | fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code) 351 | logResponseBody(rr) 352 | 353 | // Output: 354 | // # A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandler 355 | // ErrorHandler: An HTTP 400 was returned by the middleware with error message: request body has an error: value is required but missing 356 | // Received an HTTP 400 response. Expected HTTP 400 357 | // Response body: This was rewritten by the ErrorHandler 358 | } 359 | 360 | func ExampleOapiRequestValidatorWithOptions_withErrorHandlerWithOpts() { 361 | rawSpec := ` 362 | openapi: "3.0.0" 363 | info: 364 | version: 1.0.0 365 | title: TestServer 366 | servers: 367 | - url: http://example.com/ 368 | paths: 369 | /resource: 370 | post: 371 | operationId: createResource 372 | responses: 373 | '204': 374 | description: No content 375 | requestBody: 376 | required: true 377 | content: 378 | application/json: 379 | schema: 380 | properties: 381 | id: 382 | type: string 383 | minLength: 100 384 | name: 385 | type: string 386 | enum: 387 | - Marcin 388 | additionalProperties: false 389 | /protected_resource: 390 | get: 391 | operationId: getProtectedResource 392 | security: 393 | - BearerAuth: 394 | - someScope 395 | - BasicAuth: [] 396 | responses: 397 | '204': 398 | description: no content 399 | components: 400 | securitySchemes: 401 | BearerAuth: 402 | type: http 403 | scheme: bearer 404 | bearerFormat: JWT 405 | BasicAuth: 406 | type: http 407 | scheme: basic 408 | ` 409 | 410 | must := func(err error) { 411 | if err != nil { 412 | panic(err) 413 | } 414 | } 415 | 416 | use := func(r *http.ServeMux, middlewares ...func(next http.Handler) http.Handler) http.Handler { 417 | var s http.Handler 418 | s = r 419 | 420 | for _, mw := range middlewares { 421 | s = mw(s) 422 | } 423 | 424 | return s 425 | } 426 | 427 | logResponseBody := func(rr *httptest.ResponseRecorder) { 428 | if rr.Result().Body != nil { 429 | data, _ := io.ReadAll(rr.Result().Body) 430 | if len(data) > 0 { 431 | fmt.Printf("Response body: %s", data) 432 | } 433 | } 434 | } 435 | 436 | spec, err := openapi3.NewLoader().LoadFromData([]byte(rawSpec)) 437 | must(err) 438 | 439 | // 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 440 | // See also: Options#SilenceServersWarning 441 | spec.Servers = nil 442 | 443 | router := http.NewServeMux() 444 | 445 | router.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 446 | fmt.Printf("%s /resource was called\n", r.Method) 447 | 448 | if r.Method == http.MethodPost { 449 | w.WriteHeader(http.StatusNoContent) 450 | return 451 | } 452 | 453 | w.WriteHeader(http.StatusMethodNotAllowed) 454 | }) 455 | 456 | router.HandleFunc("/protected_resource", func(w http.ResponseWriter, r *http.Request) { 457 | // NOTE that we're setting up our `authenticationFunc` (below) to /never/ allow any requests in - so if we get a response from this endpoint, our `authenticationFunc` hasn't correctly worked 458 | 459 | if r.Method == http.MethodGet { 460 | w.WriteHeader(http.StatusNoContent) 461 | return 462 | } 463 | 464 | w.WriteHeader(http.StatusMethodNotAllowed) 465 | }) 466 | 467 | authenticationFunc := func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error { 468 | fmt.Printf("`AuthenticationFunc` was called for securitySchemeName=%s\n", ai.SecuritySchemeName) 469 | return fmt.Errorf("this check always fails - don't let anyone in!") 470 | } 471 | 472 | errorHandlerFunc := func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request, opts middleware.ErrorHandlerOpts) { 473 | if opts.MatchedRoute == nil { 474 | fmt.Printf("ErrorHandlerWithOpts: An HTTP %d was returned by the middleware with error message: %s\n", opts.StatusCode, err.Error()) 475 | 476 | // NOTE that you may want to override the default (an HTTP 400 Bad Request) to an HTTP 404 Not Found (or maybe an HTTP 405 Method Not Allowed, depending on what the requested resource was) 477 | http.Error(w, fmt.Sprintf("No route was found (according to ErrorHandlerWithOpts), and we changed the HTTP status code to %d", http.StatusNotFound), http.StatusNotFound) 478 | return 479 | } 480 | 481 | switch e := err.(type) { 482 | case *openapi3filter.SecurityRequirementsError: 483 | out := fmt.Sprintf("A SecurityRequirementsError was returned when attempting to authenticate the request to %s %s against %d Security Schemes: %s\n", opts.MatchedRoute.Route.Method, opts.MatchedRoute.Route.Path, len(e.SecurityRequirements), e.Error()) 484 | for _, sr := range e.SecurityRequirements { 485 | for k, v := range sr { 486 | out += fmt.Sprintf("- %s: %v\n", k, v) 487 | } 488 | } 489 | 490 | fmt.Printf("ErrorHandlerWithOpts: %s\n", out) 491 | 492 | http.Error(w, "You're not allowed!", opts.StatusCode) 493 | return 494 | case *openapi3filter.RequestError: 495 | out := fmt.Sprintf("A RequestError was returned when attempting to validate the request to %s %s: %s\n", opts.MatchedRoute.Route.Method, opts.MatchedRoute.Route.Path, e.Error()) 496 | 497 | if e.RequestBody != nil { 498 | out += "This operation has a request body, which was " 499 | if !e.RequestBody.Required { 500 | out += "not " 501 | } 502 | out += "required\n" 503 | } 504 | 505 | if childErr := e.Unwrap(); childErr != nil { 506 | out += "There was a child error, which was " 507 | switch e := childErr.(type) { 508 | case *openapi3.SchemaError: 509 | out += "a SchemaError, which failed to validate on the " + e.SchemaField + " field" 510 | default: 511 | out += "an unknown type (" + reflect.TypeOf(e).String() + ")" 512 | } 513 | } 514 | 515 | fmt.Printf("ErrorHandlerWithOpts: %s\n", out) 516 | 517 | http.Error(w, "A bad request was made - but I'm not going to tell you where or how", opts.StatusCode) 518 | return 519 | } 520 | 521 | http.Error(w, err.Error(), opts.StatusCode) 522 | } 523 | 524 | // create middleware 525 | mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{ 526 | Options: openapi3filter.Options{ 527 | AuthenticationFunc: authenticationFunc, 528 | }, 529 | ErrorHandlerWithOpts: errorHandlerFunc, 530 | }) 531 | 532 | // then wire it in 533 | server := use(router, mw) 534 | 535 | // ================================================================================ 536 | fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandlerWithOpts") 537 | 538 | req, err := http.NewRequest(http.MethodPost, "/resource", nil) 539 | must(err) 540 | req.Header.Set("Content-Type", "application/json") 541 | 542 | rr := httptest.NewRecorder() 543 | 544 | server.ServeHTTP(rr, req) 545 | 546 | fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code) 547 | logResponseBody(rr) 548 | fmt.Println() 549 | 550 | // ================================================================================ 551 | fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with an invalid request body), and is then logged by the ErrorHandlerWithOpts") 552 | 553 | body := map[string]string{ 554 | "id": "not-long-enough", 555 | } 556 | 557 | data, err := json.Marshal(body) 558 | must(err) 559 | 560 | req, err = http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(data)) 561 | must(err) 562 | req.Header.Set("Content-Type", "application/json") 563 | 564 | rr = httptest.NewRecorder() 565 | 566 | server.ServeHTTP(rr, req) 567 | 568 | fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code) 569 | logResponseBody(rr) 570 | fmt.Println() 571 | 572 | // ================================================================================ 573 | fmt.Println("# A request that to an unknown path is rejected with HTTP 404 Not Found, and is then logged by the ErrorHandlerWithOpts") 574 | 575 | req, err = http.NewRequest(http.MethodGet, "/not-a-real-path", nil) 576 | must(err) 577 | 578 | rr = httptest.NewRecorder() 579 | 580 | server.ServeHTTP(rr, req) 581 | 582 | fmt.Printf("Received an HTTP %d response. Expected HTTP 404\n", rr.Code) 583 | logResponseBody(rr) 584 | fmt.Println() 585 | 586 | // ================================================================================ 587 | fmt.Println("# A request to an authenticated endpoint must go through an `AuthenticationFunc`, and if it fails, an HTTP 401 is returned") 588 | 589 | req, err = http.NewRequest(http.MethodGet, "/protected_resource", nil) 590 | must(err) 591 | 592 | rr = httptest.NewRecorder() 593 | 594 | server.ServeHTTP(rr, req) 595 | 596 | fmt.Printf("Received an HTTP %d response. Expected HTTP 401\n", rr.Code) 597 | logResponseBody(rr) 598 | fmt.Println() 599 | 600 | // Output: 601 | // # A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandlerWithOpts 602 | // ErrorHandlerWithOpts: A RequestError was returned when attempting to validate the request to POST /resource: request body has an error: value is required but missing 603 | // This operation has a request body, which was required 604 | // There was a child error, which was an unknown type (*errors.errorString) 605 | // Received an HTTP 400 response. Expected HTTP 400 606 | // Response body: A bad request was made - but I'm not going to tell you where or how 607 | // 608 | // # A request that is malformed is rejected with HTTP 400 Bad Request (with an invalid request body), and is then logged by the ErrorHandlerWithOpts 609 | // ErrorHandlerWithOpts: A RequestError was returned when attempting to validate the request to POST /resource: request body has an error: doesn't match schema: Error at "/id": minimum string length is 100 610 | // Schema: 611 | // { 612 | // "minLength": 100, 613 | // "type": "string" 614 | // } 615 | // 616 | // Value: 617 | // "not-long-enough" 618 | // 619 | // This operation has a request body, which was required 620 | // There was a child error, which was a SchemaError, which failed to validate on the minLength field 621 | // Received an HTTP 400 response. Expected HTTP 400 622 | // Response body: A bad request was made - but I'm not going to tell you where or how 623 | // 624 | // # A request that to an unknown path is rejected with HTTP 404 Not Found, and is then logged by the ErrorHandlerWithOpts 625 | // ErrorHandlerWithOpts: An HTTP 404 was returned by the middleware with error message: no matching operation was found 626 | // Received an HTTP 404 response. Expected HTTP 404 627 | // Response body: No route was found (according to ErrorHandlerWithOpts), and we changed the HTTP status code to 404 628 | // 629 | // # A request to an authenticated endpoint must go through an `AuthenticationFunc`, and if it fails, an HTTP 401 is returned 630 | // `AuthenticationFunc` was called for securitySchemeName=BearerAuth 631 | // `AuthenticationFunc` was called for securitySchemeName=BasicAuth 632 | // ErrorHandlerWithOpts: A SecurityRequirementsError was returned when attempting to authenticate the request to GET /protected_resource against 2 Security Schemes: security requirements failed: this check always fails - don't let anyone in! | this check always fails - don't let anyone in! 633 | // - BearerAuth: [someScope] 634 | // - BasicAuth: [] 635 | // 636 | // Received an HTTP 401 response. Expected HTTP 401 637 | // Response body: You're not allowed! 638 | } 639 | 640 | func ExampleOapiRequestValidatorWithOptions_withErrorHandlerWithOptsAndMultiError() { 641 | rawSpec := ` 642 | openapi: "3.0.0" 643 | info: 644 | version: 1.0.0 645 | title: TestServer 646 | servers: 647 | - url: http://example.com/ 648 | paths: 649 | /resource: 650 | post: 651 | operationId: createResource 652 | responses: 653 | '204': 654 | description: No content 655 | requestBody: 656 | required: true 657 | content: 658 | application/json: 659 | schema: 660 | properties: 661 | id: 662 | type: string 663 | minLength: 100 664 | name: 665 | type: string 666 | enum: 667 | - Marcin 668 | additionalProperties: false 669 | ` 670 | 671 | must := func(err error) { 672 | if err != nil { 673 | panic(err) 674 | } 675 | } 676 | 677 | use := func(r *http.ServeMux, middlewares ...func(next http.Handler) http.Handler) http.Handler { 678 | var s http.Handler 679 | s = r 680 | 681 | for _, mw := range middlewares { 682 | s = mw(s) 683 | } 684 | 685 | return s 686 | } 687 | 688 | logResponseBody := func(rr *httptest.ResponseRecorder) { 689 | if rr.Result().Body != nil { 690 | data, _ := io.ReadAll(rr.Result().Body) 691 | if len(data) > 0 { 692 | fmt.Printf("Response body: %s", data) 693 | } 694 | } 695 | } 696 | 697 | spec, err := openapi3.NewLoader().LoadFromData([]byte(rawSpec)) 698 | must(err) 699 | 700 | // 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 701 | // See also: Options#SilenceServersWarning 702 | spec.Servers = nil 703 | 704 | router := http.NewServeMux() 705 | 706 | router.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { 707 | fmt.Printf("%s /resource was called\n", r.Method) 708 | 709 | if r.Method == http.MethodPost { 710 | w.WriteHeader(http.StatusNoContent) 711 | return 712 | } 713 | 714 | w.WriteHeader(http.StatusMethodNotAllowed) 715 | }) 716 | 717 | errorHandlerFunc := func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request, opts middleware.ErrorHandlerOpts) { 718 | if opts.MatchedRoute == nil { 719 | fmt.Printf("ErrorHandlerWithOpts: An HTTP %d was returned by the middleware with error message: %s\n", opts.StatusCode, err.Error()) 720 | 721 | // NOTE that you may want to override the default (an HTTP 400 Bad Request) to an HTTP 404 Not Found (or maybe an HTTP 405 Method Not Allowed, depending on what the requested resource was) 722 | http.Error(w, fmt.Sprintf("No route was found (according to ErrorHandlerWithOpts), and we changed the HTTP status code to %d", http.StatusNotFound), http.StatusNotFound) 723 | return 724 | } 725 | 726 | switch e := err.(type) { 727 | // NOTE that when it's a MultiError, there's more work needed here 728 | case openapi3.MultiError: 729 | var re *openapi3filter.RequestError 730 | if e.As(&re) { 731 | out := fmt.Sprintf("A MultiError was encountered, which contained a RequestError: %s", re) 732 | 733 | if re.Err != nil { 734 | out += ", which inside it has a error of type (" + reflect.TypeOf(e).String() + ")" 735 | } 736 | 737 | fmt.Printf("ErrorHandlerWithOpts: %s\n", out) 738 | 739 | http.Error(w, "There was a bad request", opts.StatusCode) 740 | return 741 | } 742 | 743 | var se *openapi3filter.SecurityRequirementsError 744 | if e.As(&se) { 745 | out := fmt.Sprintf("A MultiError was encountered, which contained a SecurityRequirementsError: %s", re) 746 | 747 | if len(se.Errors) > 0 { 748 | out += fmt.Sprintf(", which contains %d child errors", len(se.Errors)) 749 | } 750 | 751 | fmt.Printf("ErrorHandlerWithOpts: %s\n", out) 752 | 753 | http.Error(w, "There was an unauthorized request", opts.StatusCode) 754 | return 755 | } 756 | } 757 | 758 | http.Error(w, err.Error(), opts.StatusCode) 759 | } 760 | 761 | // create middleware 762 | mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{ 763 | Options: openapi3filter.Options{ 764 | // make sure that multiple errors in a given request are returned 765 | MultiError: true, 766 | }, 767 | ErrorHandlerWithOpts: errorHandlerFunc, 768 | }) 769 | 770 | // then wire it in 771 | server := use(router, mw) 772 | 773 | // ================================================================================ 774 | fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandlerWithOpts") 775 | 776 | req, err := http.NewRequest(http.MethodPost, "/resource", nil) 777 | must(err) 778 | req.Header.Set("Content-Type", "application/json") 779 | 780 | rr := httptest.NewRecorder() 781 | 782 | server.ServeHTTP(rr, req) 783 | 784 | fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code) 785 | logResponseBody(rr) 786 | fmt.Println() 787 | 788 | // ================================================================================ 789 | fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with an invalid request body, with multiple issues), and is then logged by the ErrorHandlerWithOpts") 790 | 791 | body := map[string]string{ 792 | "id": "not-long-enough", 793 | "name": "Jamie", 794 | } 795 | 796 | data, err := json.Marshal(body) 797 | must(err) 798 | 799 | req, err = http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(data)) 800 | must(err) 801 | req.Header.Set("Content-Type", "application/json") 802 | 803 | rr = httptest.NewRecorder() 804 | 805 | server.ServeHTTP(rr, req) 806 | 807 | fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code) 808 | logResponseBody(rr) 809 | fmt.Println() 810 | 811 | // Output: 812 | // # A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandlerWithOpts 813 | // ErrorHandlerWithOpts: A MultiError was encountered, which contained a RequestError: request body has an error: value is required but missing, which inside it has a error of type (openapi3.MultiError) 814 | // Received an HTTP 400 response. Expected HTTP 400 815 | // Response body: There was a bad request 816 | // 817 | // # A request that is malformed is rejected with HTTP 400 Bad Request (with an invalid request body, with multiple issues), and is then logged by the ErrorHandlerWithOpts 818 | // ErrorHandlerWithOpts: A MultiError was encountered, which contained a RequestError: request body has an error: doesn't match schema: Error at "/id": minimum string length is 100 819 | // Schema: 820 | // { 821 | // "minLength": 100, 822 | // "type": "string" 823 | // } 824 | // 825 | // Value: 826 | // "not-long-enough" 827 | // | Error at "/name": value is not one of the allowed values ["Marcin"] 828 | // Schema: 829 | // { 830 | // "enum": [ 831 | // "Marcin" 832 | // ], 833 | // "type": "string" 834 | // } 835 | // 836 | // Value: 837 | // "Jamie" 838 | // , which inside it has a error of type (openapi3.MultiError) 839 | // Received an HTTP 400 response. Expected HTTP 400 840 | // Response body: There was a bad request 841 | } 842 | --------------------------------------------------------------------------------