Repository: avenga/couper
Pull request #235 (2021-05-18 08:50:15)

fixed typos


Patch:
diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md
new file mode 100755
index 00000000..a6a15b0c
--- /dev/null
+++ b/.chglog/CHANGELOG.tpl.md
@@ -0,0 +1,34 @@
+# Couper Changelog
+
+{{ range .Versions }}
+<a name="{{ .Tag.Name }}"></a>
+## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }}
+
+> {{ datetime "2006-01-02" .Tag.Date }}
+
+{{ range .CommitGroups -}}
+### {{ .Title }}
+
+{{ range .Commits -}}
+* {{ .Subject }}
+{{ end }}
+{{ end -}}
+
+{{- if .RevertCommits -}}
+### Reverts
+
+{{ range .RevertCommits -}}
+* {{ .Revert.Header }}
+{{ end }}
+{{ end -}}
+
+{{- if .NoteGroups -}}
+{{ range .NoteGroups -}}
+### {{ .Title }}
+
+{{ range .Notes }}
+{{ .Body }}
+{{ end }}
+{{ end -}}
+{{ end -}}
+{{ end -}}
diff --git a/.chglog/config.yml b/.chglog/config.yml
new file mode 100755
index 00000000..55852ece
--- /dev/null
+++ b/.chglog/config.yml
@@ -0,0 +1,31 @@
+style: github
+template: CHANGELOG.tpl.md
+info:
+  title: CHANGELOG
+  repository_url: https://github.com/avenga/couper
+options:
+  commits:
+    # filters:
+    #   Type:
+    #     - feat
+    #     - fix
+    #     - perf
+    #     - refactor
+  commit_groups:
+     title_maps:
+       feat: Features
+       Add: Features
+       add: Features
+       change: Changes
+       Fix: Bug Fixes
+       Fixup: Bug Fixes
+       perf: Performance Improvements
+       refactor: Code Refactoring
+  header:
+    pattern: "^((\\w+)\\s.*)$"
+    pattern_maps:
+      - Subject
+      - Type
+  notes:
+    keywords:
+      - BREAKING CHANGE
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59e725b9..e21ff2e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,54 +1,11 @@
 # Couper Changelog
 
-<a name="unreleased"></a>
-## [Unreleased](https://github.com/avenga/couper/compare/1.2...master)
-
-Release date: 2021-xx-xx
-
-Unreleased changes are available as `avenga/couper:edge` container.
-
----
-
-<a name="1.2"></a>
-## [1.2](https://github.com/avenga/couper/compare/1.1.1...1.2)
-
-Release date: 2021-05-18
-
-The most important feature of Couper 1.2 is the introduction of _custom
-error handling_ in form of the [`error_handler`](/docs/ERRORS.md) block.
-You can now register error handlers for [error types](/docs/ERRORS.md#error-types). Instead of the standard `error_file` template,
-you can flexibly respond with arbitrary `response`s. `error_handler` is allowed in access control blocks (`jwt`, `saml2` …), where you
-could e.g. handle missing tokens with a redirect-to-login. In the
-future, `error_handler` will be usable in more config areas. Refer to
-the [example](https://github.com/avenga/couper-examples/tree/master/error-handling-ba)
-if you want to see it in action.
-
-### Added
-
-* `error_handler` block for access controls ([#140](https://github.com/avenga/couper/pull/140))
-* `backend_responses.*.body` variable for accessing raw response body content ([#182](https://github.com/avenga/couper/issues/182))
-* more `oauth2` config options: `scope` and `token_endpoint_auth_method` (`client_secret_basic` or `client_secret_post`) ([#219](https://github.com/avenga/couper/pull/219), [#220](https://github.com/avenga/couper/pull/220))
-
-### Changed
-
-* `saml2` fallback to `nameid-format:unspecified` ([#217](https://github.com/avenga/couper/pull/217))
-* `basic_auth` always responds with status code `401` ([#227](https://github.com/avenga/couper/pull/227))
-* `openapi` resolves relative server URLs to the current backend origin ([#230](https://github.com/avenga/couper/pull/230))
-
-### Fixed
-
-* Fix `/healthz` route when called with `accept-encoding: gzip` ([#222](https://github.com/avenga/couper/pull/222))
-* Don't panic over duplicate access control definitions, log error instead ([#221](https://github.com/avenga/couper/pull/221))
-* Response for missing routes should have status code `404` ([#224](https://github.com/avenga/couper/pull/224))
-* Fix possible race-condition with concurrent `openapi` validations ([#231](https://github.com/avenga/couper/pull/231))
-* Fix use of server URLs without port in `openapi` ([#230](https://github.com/avenga/couper/pull/230))
-
 <a name="1.1.1"></a>
 ## [1.1.1](https://github.com/avenga/couper/compare/1.1...1.1.1)
 
 > 2021-04-21
 
-### Fixed
+### Bug Fixes
 
 * Endpoint responses are written and logged with correct status-code ([#216](https://github.com/avenga/couper/issues/216))
   * affected: a plain `response` without any additional headers or body configuration
@@ -58,7 +15,7 @@ if you want to see it in action.
 
 > 2021-04-16
 
-### Fixed
+### Bug Fixes
 
 * allow more +json mime types ([#207](https://github.com/avenga/couper/pull/207))
   * determines if ja request/response body gets parsed and provided as `json_body` variable
@@ -77,7 +34,7 @@ if you want to see it in action.
 * Change access control validation logging ([#199](https://github.com/avenga/couper/issues/199))
   * log the first occurred error instead of an array
 
-### Added
+### Features
 
 * Add OAuth2 token request retry option  ([#167](https://github.com/avenga/couper/issues/167)) ([#209](https://github.com/avenga/couper/issues/209))
 
@@ -87,16 +44,16 @@ if you want to see it in action.
 
 > 2021-04-09
 
-### Added
+### Features
 
 * `couper help` and usage documentation ([#187](https://github.com/avenga/couper/issues/187))
 
-### Changed
+### Changes
 
 * Ensure unique keys for set_* and add_* attributes ([#183](https://github.com/avenga/couper/pull/183))
 * split docker entrypoint and command ([#192](https://github.com/avenga/couper/issues/192))
 
-### Fixed
+### Bug Fixes
 
 * Fix missing `backend.origin` attribute url validation ([#191](https://github.com/avenga/couper/issues/191))
 
@@ -106,7 +63,7 @@ if you want to see it in action.
 
 > 2021-04-08
 
-### Fixed
+### Bug Fixes
 
 * Log option for `json` formatted logs: ([#176](https://github.com/avenga/couper/issues/176))
   * configured parent key applies to (almost) all log fields 
@@ -123,7 +80,7 @@ if you want to see it in action.
 * Log option for parent fields are 'global' now ([#176](https://github.com/avenga/couper/issues/176))
   * `COUPER_ACCESS_LOG_PARENT_FIELD`, `COUPER_BACKEND_LOG_PARENT_FIELD` -> `COUPER_LOG_PARENT_FIELD`
 
-### Added
+### Features
 
 * watch option for given Couper configuration file ([#24](https://github.com/avenga/couper/issues/24))
   * use `-watch` or via environment `COUPER_WATCH=true` to watch for file changes
@@ -135,7 +92,7 @@ if you want to see it in action.
 
 > 2021-04-06
 
-### Fixed
+### Bug Fixes
 
 * Some possible race conditions in combination with multiple `proxy` and/or `request`
   definitions are fixed ([#157](https://github.com/avenga/couper/issues/177)) ([#160](https://github.com/avenga/couper/issues/160))
@@ -151,12 +108,12 @@ if you want to see it in action.
 * 404 behaviour for access controlled endpoints:
   deny instead of 404 if the request matches the related base_path ([#143](https://github.com/avenga/couper/issues/143))
 
-### Changed
+### Changes
 
 * Rename log type for backend requests: `couper_upstream` -> `couper_backend` ([#159](https://github.com/avenga/couper/pull/159)) ([#172](https://github.com/avenga/couper/pull/172))
 * Rename `post` variable to `form_body` ([#158](https://github.com/avenga/couper/pull/158))
 
-### Added
+### Features
 
 * Add `json_body` attribute for `request` and `response` block ([#158](https://github.com/avenga/couper/issues/158))
 * `bytes` log field to represent the body size
@@ -167,12 +124,12 @@ if you want to see it in action.
 
 > 2021-03-23
 
-### Fixed
+### Bug Fixes
 
 * Recover from possible request/proxy related panics ([#157](https://github.com/avenga/couper/pull/157)) ([#145](https://github.com/avenga/couper/pull/145))
 * Configuration related hcl merge with an empty attributes and nested blocks
 
-### Changed
+### Change
 
 * `backend` block attributes `basic_auth`, `path_prefix` and `proxy` hcl evaluation during runtime
 * `request` attributes hcl evaluation during runtime ([#152](https://github.com/avenga/couper/pull/152))
@@ -182,7 +139,7 @@ if you want to see it in action.
 * no `X-Forwarded-For` header enrichment from couper `proxy` ([#139](https://github.com/avenga/couper/pull/139))
 * more log context for access control related errors ([#154](https://github.com/avenga/couper/issues/154))
 
-### Added
+### Features
 
 * `saml` 2.0 `access_control` support ([#113](https://github.com/avenga/couper/issues/113))
 * Add new `strip-secure-cookies` setting ([#147](https://github.com/avenga/couper/issues/147))
@@ -204,17 +161,17 @@ if you want to see it in action.
 
 > 2021-03-15
 
-### Fixed
+### Bug Fixes
 
 * Fix missing panic recovering for backend roundtrips ([#142](https://github.com/avenga/couper/issues/142))
   * Fix backend `timeout` behaviour
   * Add a more specific error message for proxy body copy errors
 
-### Changed
+### Change
 
 * Couper just passes the `X-Forwarded-For` header if any instead of adding the client remote addr to the list ([#139](https://github.com/avenga/couper/pull/139))
 
-### Added
+### Features
 
 * `url_encode` function for RFC 3986 string encoding ([#136](https://github.com/avenga/couper/pull/136))
 
@@ -251,18 +208,18 @@ endpoint "/new" {
 }
 ```
 
-### Changed
+### Change
 
 * Client-Request and upstream response body buffering by default
 * Server shutdown delay and deadline defaults to `0s` now and can be configured via [env](DOCKER.md) if required
 * Websocket connection upgrades in combination with `proxy {}` are disabled
   * we will add a proxy option for ws usage later on
 
-### Fixed
+### Bug Fixes
 
 * An absolute path resolving for `*_file` configuration attributes ([#120](https://github.com/avenga/couper/pull/120))
 
-### Added
+### Features
 
 * Endpoint:
   * Add `proxy` block to reverse proxy the client request to the configured `backend`.
@@ -290,14 +247,14 @@ endpoint "/new" {
 
 > 2021-02-16
 
-### Added
+### Features
 
 * backend:
   * a user-friendly `basic_auth` option
   * backend `proxy` url, `disable_connection_reuse` and `http2` settings ([#108](https://github.com/avenga/couper/pull/108))
 * version command
 
-### Changed
+### Change
 
 * KeepAlive `60s` ([#108](https://github.com/avenga/couper/pull/108)), previously `15s`
 * Reject requests which hits an endpoint with basic-auth access-control, and the configured password evaluates to an empty string ([#115](https://github.com/avenga/couper/pull/115))
@@ -307,11 +264,11 @@ endpoint "/new" {
 
 > 2021-01-29
 
-### Fixed
+### Bug Fixes
 
 * Fix missing http.Hijacker interface to be able to handle websocket upgrades ([#80](https://github.com/avenga/couper/issues/80))
 
-### Added
+### Features
 
 * Add additional eval functions: coalesce, json_decode, json_encode ([#105](https://github.com/avenga/couper/pull/105))
 * Add multi API support ([#103](https://github.com/avenga/couper/issues/103))
@@ -334,7 +291,7 @@ endpoint "/new" {
 
 > 2021-01-19
 
-### Fixed
+### Fix
 
 * Fix used backend hash not dependent on (hcl) config hierarchy (transport key)
 * Fix logging http scheme even without a successful tls handshake ([#99](https://github.com/avenga/couper/pull/99))
@@ -345,7 +302,7 @@ endpoint "/new" {
 
 > 2021-01-18
 
-### Fixed
+### Fix
 
 * Fix path trailing slash ([#94](https://github.com/avenga/couper/issues/94))
 * Fix query encoding ([#93](https://github.com/avenga/couper/issues/93))
@@ -356,13 +313,13 @@ endpoint "/new" {
 
 > 2021-01-13
 
-### Added
+### Add
 
 * url log field ([#87](https://github.com/avenga/couper/issues/87))
 * Add proxy from env settings option ([#84](https://github.com/avenga/couper/issues/84))
 * Add backend settings:  `disable_certificate_validation`, `max_connections` ([#86](https://github.com/avenga/couper/issues/86))
 
-### Fixed
+### Fix
 
 * command flag filter for bool values ([#85](https://github.com/avenga/couper/issues/85))
 * different proxy options for same origin should be part of the origin transport key
@@ -376,7 +333,7 @@ endpoint "/new" {
 
 > 2020-12-15
 
-### Added
+### Add
 
 * build version to startup log
 * upstream request/response validation with `openapi` ([#21](https://github.com/avenga/couper/issues/21)) ([#22](https://github.com/avenga/couper/issues/22))
@@ -386,7 +343,7 @@ endpoint "/new" {
 * `query_params` ([#73](https://github.com/avenga/couper/issues/73))
 * `json_body` access for request and response bodies [#44](https://github.com/avenga/couper/issues/44) ([#60](https://github.com/avenga/couper/issues/60))
 
-### Changed
+### Change
 
 * start Couper via `run` command now
 * internal router [#59](https://github.com/avenga/couper/issues/59)
@@ -395,7 +352,7 @@ endpoint "/new" {
 * passing the filename to underlying hcl diagnostics
 * Dockerfile to provide simple file serving ([#63](https://github.com/avenga/couper/issues/63))
 
-### Fixed
+### Fix
 
 * handling cty null or unknown values during roundtrip eval [#71](https://github.com/avenga/couper/issues/71)
 * logging: start-time measurement
@@ -407,14 +364,14 @@ endpoint "/new" {
 
 > 2020-10-08
 
-### Added
+### Add
 
 * health check ([#29](https://github.com/avenga/couper/issues/29))
 * Basic-Auth support ([#19](https://github.com/avenga/couper/issues/19))
 * post (form) parsing for use in config variables ([#26](https://github.com/avenga/couper/issues/26))
 * more documentation
 
-### Fixed
+### Fix
 
 * wildcard path join with trailing slash and respect req path ([#45](https://github.com/avenga/couper/pull/45))
 * env var mapping ([#35](https://github.com/avenga/couper/pull/35))
@@ -426,7 +383,7 @@ endpoint "/new" {
 
 > 2020-09-11
 
-### Added
+### Add
 
 * Parse and load from given *HCL* configuration file
 * Config structs for blocks: `server, api, endpoint, files, spa, definitions, jwt`
diff --git a/Makefile b/Makefile
index c983e33a..0f1ca079 100644
--- a/Makefile
+++ b/Makefile
@@ -31,3 +31,7 @@ test-coverage-show:
 	go tool cover -html=handler.coverage
 	go tool cover -html=server.coverage
 	go tool cover -html=main.coverage
+
+# TAG=v0.3 make changelog
+changelog:
+	git-chglog --next-tag $(TAG) $(TAG)
diff --git a/docs/ERRORS.md b/docs/ERRORS.md
index 3034d3d9..50e68eda 100644
--- a/docs/ERRORS.md
+++ b/docs/ERRORS.md
@@ -10,7 +10,7 @@
 ## Introduction
 
 Errors can occur in various places: due to invalid client requests or problems on the backend and network side.
-Couper specifies some generic error categories (like `configuration`, `server`, `backend` or `access_control`) to help you indetify the occuring problems faster.
+Couper specifies some generic error categories (like `configuration`, `server`, `backend` or `access_control`) to help you identify the occurring problems faster.
 
 
 ## Error messages
diff --git a/handler/transport/backend.go b/handler/transport/backend.go
index fab038f5..c8ecce15 100644
--- a/handler/transport/backend.go
+++ b/handler/transport/backend.go
@@ -83,6 +83,8 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 		return nil, err
 	}
 
+	t := Get(tc)
+
 	deadlineErr := b.withTimeout(req)
 
 	req.URL.Host = tc.Origin
@@ -103,6 +105,12 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 		req.Header.Del(AcceptEncodingHeader)
 	}
 
+	if b.openAPIValidator != nil {
+		if err = b.openAPIValidator.ValidateRequest(req); err != nil {
+			return nil, errors.BackendValidation.Label(b.name).With(err)
+		}
+	}
+
 	if xff, ok := req.Context().Value(request.XFF).(string); ok {
 		if xff != "" {
 			req.Header.Set("X-Forwarded-For", xff)
@@ -120,15 +128,22 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 	ctx := context.WithValue(req.Context(), request.BackendURL, req.URL.String())
 	*req = *req.WithContext(ctx)
 
-	var beresp *http.Response
-	if b.openAPIValidator != nil {
-		beresp, err = b.openAPIValidate(req, tc, deadlineErr)
-	} else {
-		beresp, err = b.innerRoundTrip(req, tc, deadlineErr)
+	beresp, err := t.RoundTrip(req)
+	if err != nil {
+		select {
+		case derr := <-deadlineErr:
+			if derr != nil {
+				return nil, derr
+			}
+		default:
+			return nil, errors.Backend.Label(b.name).With(err)
+		}
 	}
 
-	if err != nil {
-		return nil, err
+	if b.openAPIValidator != nil {
+		if err = b.openAPIValidator.ValidateResponse(beresp); err != nil {
+			return nil, errors.BackendValidation.Label(b.name).With(err).Status(http.StatusBadGateway)
+		}
 	}
 
 	if strings.ToLower(beresp.Header.Get(ContentEncodingHeader)) == GzipName {
@@ -156,43 +171,6 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 	return beresp, err
 }
 
-func (b *Backend) openAPIValidate(req *http.Request, tc *Config, deadlineErr <-chan error) (*http.Response, error) {
-	requestValidationInput, err := b.openAPIValidator.ValidateRequest(req, tc.hash(), tc.Scheme+"://"+tc.Origin)
-	if err != nil {
-		return nil, errors.BackendValidation.Label(b.name).Kind("backend_request_validation").With(err)
-	}
-
-	beresp, err := b.innerRoundTrip(req, tc, deadlineErr)
-	if err != nil {
-		return nil, err
-	}
-
-	if err = b.openAPIValidator.ValidateResponse(beresp, requestValidationInput); err != nil {
-		return nil, errors.BackendValidation.Label(b.name).Kind("backend_response_validation").
-			With(err).Status(http.StatusBadGateway)
-	}
-
-	return beresp, nil
-}
-
-func (b *Backend) innerRoundTrip(req *http.Request, tc *Config, deadlineErr <-chan error) (*http.Response, error) {
-	t := Get(tc)
-	beresp, err := t.RoundTrip(req)
-
-	if err != nil {
-		select {
-		case derr := <-deadlineErr:
-			if derr != nil {
-				return nil, derr
-			}
-		default:
-			return nil, errors.Backend.Label(b.name).With(err)
-		}
-	}
-
-	return beresp, nil
-}
-
 func (b *Backend) withPathPrefix(req *http.Request) {
 	if pathPrefix := b.getAttribute(req, "path_prefix"); pathPrefix != "" {
 		req.URL.Path = utils.JoinPath("/", pathPrefix, req.URL.Path)
diff --git a/handler/validation/openapi.go b/handler/validation/openapi.go
index 2e25ba14..facbf574 100644
--- a/handler/validation/openapi.go
+++ b/handler/validation/openapi.go
@@ -6,20 +6,17 @@ import (
 	"io"
 	"io/ioutil"
 	"net/http"
-	"net/url"
-	"sync"
 
-	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/avenga/couper/config/request"
+
 	"github.com/getkin/kin-openapi/openapi3filter"
 
-	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/eval"
 )
 
-var swaggers sync.Map
-
 type OpenAPI struct {
-	options *OpenAPIOptions
+	options                *OpenAPIOptions
+	requestValidationInput *openapi3filter.RequestValidationInput
 }
 
 func NewOpenAPI(opts *OpenAPIOptions) *OpenAPI {
@@ -31,73 +28,20 @@ func NewOpenAPI(opts *OpenAPIOptions) *OpenAPI {
 	}
 }
 
-func (v *OpenAPI) getModifiedSwagger(key, origin string) (*openapi3.Swagger, error) {
-	swagger, exists := swaggers.Load(key)
-	if !exists {
-		clonedSwagger := cloneSwagger(v.options.swagger)
-
-		var newServers []string
-		for _, s := range clonedSwagger.Servers {
-			su, err := url.Parse(s.URL)
-			if err != nil {
-				return nil, err
-			}
-			if !su.IsAbs() {
-				newServers = append(newServers, origin+s.URL)
-			}
-		}
-		for _, ns := range newServers {
-			clonedSwagger.AddServer(&openapi3.Server{URL: ns})
-		}
-
-		swaggers.Store(key, clonedSwagger)
-		swagger = clonedSwagger
-	}
-
-	if s, ok := swagger.(*openapi3.Swagger); ok {
-		return s, nil
-	}
-
-	return nil, fmt.Errorf("swagger wrong type: %v", swagger)
-}
-
-func cloneSwagger(s *openapi3.Swagger) *openapi3.Swagger {
-	sw := *s
-	// this is not a deep clone; we only want to add servers
-	sw.Servers = s.Servers[:]
-	return &sw
-}
-
-func (v *OpenAPI) ValidateRequest(req *http.Request, key, origin string) (*openapi3filter.RequestValidationInput, error) {
-	swagger, err := v.getModifiedSwagger(key, origin)
+func (v *OpenAPI) ValidateRequest(req *http.Request) error {
+	route, pathParams, err := v.options.router.FindRoute(req.Method, req.URL)
 	if err != nil {
+		err = fmt.Errorf("request validation: '%s %s': %w", req.Method, req.URL.Path, err)
 		if ctx, ok := req.Context().Value(request.OpenAPI).(*OpenAPIContext); ok {
 			ctx.errors = append(ctx.errors, err)
 		}
 		if !v.options.ignoreRequestViolations {
-			return nil, err
-		}
-		return nil, nil
-	}
-
-	router := openapi3filter.NewRouter()
-	if err = router.AddSwagger(swagger); err != nil {
-		return nil, err
-	}
-
-	route, pathParams, err := router.FindRoute(req.Method, req.URL)
-	if err != nil {
-		err = fmt.Errorf("'%s %s': %w", req.Method, req.URL.Path, err)
-		if ctx, ok := req.Context().Value(request.OpenAPI).(*OpenAPIContext); ok {
-			ctx.errors = append(ctx.errors, err)
-		}
-		if !v.options.ignoreRequestViolations {
-			return nil, err
+			return err
 		}
-		return nil, nil
+		return nil
 	}
 
-	requestValidationInput := &openapi3filter.RequestValidationInput{
+	v.requestValidationInput = &openapi3filter.RequestValidationInput{
 		Options:     v.options.filterOptions,
 		PathParams:  pathParams,
 		QueryParams: req.URL.Query(),
@@ -106,22 +50,25 @@ func (v *OpenAPI) ValidateRequest(req *http.Request, key, origin string) (*opena
 	}
 
 	// openapi3filter.ValidateRequestBody also handles resetting the req body after reading until EOF.
-	if err = openapi3filter.ValidateRequest(req.Context(), requestValidationInput); err != nil {
+	err = openapi3filter.ValidateRequest(req.Context(), v.requestValidationInput)
+
+	if err != nil {
+		err = fmt.Errorf("request validation: %w", err)
 		if ctx, ok := req.Context().Value(request.OpenAPI).(*OpenAPIContext); ok {
 			ctx.errors = append(ctx.errors, err)
 		}
 		if !v.options.ignoreRequestViolations {
-			return requestValidationInput, err
+			return err
 		}
 	}
 
-	return requestValidationInput, nil
+	return nil
 }
 
-func (v *OpenAPI) ValidateResponse(beresp *http.Response, requestValidationInput *openapi3filter.RequestValidationInput) error {
+func (v *OpenAPI) ValidateResponse(beresp *http.Response) error {
 	// since a request validation could fail and ignored due to user options, the input route MAY be nil
-	if requestValidationInput == nil || requestValidationInput.Route == nil {
-		err := fmt.Errorf("'%s %s': invalid route", beresp.Request.Method, beresp.Request.URL.Path)
+	if v.requestValidationInput == nil || v.requestValidationInput.Route == nil {
+		err := fmt.Errorf("response validation: '%s %s': invalid route", beresp.Request.Method, beresp.Request.URL.Path)
 		if beresp.Request != nil {
 			if ctx, ok := beresp.Request.Context().Value(request.OpenAPI).(*OpenAPIContext); ok {
 				ctx.errors = append(ctx.errors, err)
@@ -138,7 +85,7 @@ func (v *OpenAPI) ValidateResponse(beresp *http.Response, requestValidationInput
 		Body:                   ioutil.NopCloser(&bytes.Buffer{}),
 		Header:                 beresp.Header.Clone(),
 		Options:                v.options.filterOptions,
-		RequestValidationInput: requestValidationInput,
+		RequestValidationInput: v.requestValidationInput,
 		Status:                 beresp.StatusCode,
 	}
 
@@ -156,6 +103,7 @@ func (v *OpenAPI) ValidateResponse(beresp *http.Response, requestValidationInput
 	}
 
 	if err := openapi3filter.ValidateResponse(beresp.Request.Context(), responseValidationInput); err != nil {
+		err = fmt.Errorf("response validation: %w", err)
 		if beresp.Request != nil {
 			if ctx, ok := beresp.Request.Context().Value(request.OpenAPI).(*OpenAPIContext); ok {
 				ctx.errors = append(ctx.errors, err)
diff --git a/handler/validation/openapi_options.go b/handler/validation/openapi_options.go
index 0ffbdad5..74805275 100644
--- a/handler/validation/openapi_options.go
+++ b/handler/validation/openapi_options.go
@@ -3,7 +3,6 @@ package validation
 import (
 	"fmt"
 	"io/ioutil"
-	"net/url"
 	"path/filepath"
 
 	"github.com/getkin/kin-openapi/openapi3"
@@ -18,7 +17,7 @@ type OpenAPIOptions struct {
 	ignoreRequestViolations  bool
 	ignoreResponseViolations bool
 	filterOptions            *openapi3filter.Options
-	swagger                  *openapi3.Swagger
+	router                   *openapi3filter.Router
 }
 
 // NewOpenAPIOptions takes a list of openAPI configuration due to merging configurations.
@@ -41,26 +40,6 @@ func NewOpenAPIOptions(openapi *config.OpenAPI) (*OpenAPIOptions, error) {
 	return NewOpenAPIOptionsFromBytes(openapi, b)
 }
 
-func canonicalizeServerURLs(swagger *openapi3.Swagger) error {
-	for _, server := range swagger.Servers {
-		su, err := url.Parse(server.URL)
-		if err != nil {
-			return err
-		}
-
-		if su.IsAbs() && su.Port() == "" && (su.Scheme == "https" || su.Scheme == "http") {
-			su.Host = su.Hostname() + ":"
-			if su.Scheme == "https" {
-				su.Host += "443"
-			} else {
-				su.Host += "80"
-			}
-			server.URL = su.String()
-		}
-	}
-	return nil
-}
-
 func NewOpenAPIOptionsFromBytes(openapi *config.OpenAPI, bytes []byte) (*OpenAPIOptions, error) {
 	if openapi == nil || bytes == nil {
 		return nil, nil
@@ -71,7 +50,8 @@ func NewOpenAPIOptionsFromBytes(openapi *config.OpenAPI, bytes []byte) (*OpenAPI
 		return nil, fmt.Errorf("error loading openapi file: %w", err)
 	}
 
-	if err = canonicalizeServerURLs(swagger); err != nil {
+	router := openapi3filter.NewRouter()
+	if err = router.AddSwagger(swagger); err != nil {
 		return nil, err
 	}
 
@@ -88,6 +68,6 @@ func NewOpenAPIOptionsFromBytes(openapi *config.OpenAPI, bytes []byte) (*OpenAPI
 		},
 		ignoreRequestViolations:  openapi.IgnoreRequestViolations,
 		ignoreResponseViolations: openapi.IgnoreResponseViolations,
-		swagger:                  swagger,
+		router:                   router,
 	}, nil
 }


Test patch:
diff --git a/changelog_test.go b/changelog_test.go
deleted file mode 100644
index 486c92e3..00000000
--- a/changelog_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package main
-
-import (
-	"bufio"
-	"bytes"
-	"io"
-	"os"
-	"regexp"
-	"testing"
-
-	"github.com/avenga/couper/internal/test"
-)
-
-// TestChangelog_Links ensures that every added/changed/fixed listing has a link (pr/issue).
-func TestChangelog_Links(t *testing.T) {
-	helper := test.New(t)
-
-	changelog, err := os.Open("CHANGELOG.md")
-	helper.Must(err)
-	defer changelog.Close()
-
-	r := bufio.NewReader(changelog)
-
-	linkRegex := regexp.MustCompile(`\(\[#\d+]\(.+/(pull|issues)/\d+\)\)\n$`)
-
-	nr := 0
-	for {
-		nr++
-		line, readErr := r.ReadSlice('\n')
-		if readErr == io.EOF {
-			return
-		}
-		helper.Must(readErr)
-
-		// Link Condition introduced with 1.2 release.
-		if bytes.Equal(line, []byte("> 2021-04-21\n")) {
-			return
-		}
-
-		if !bytes.HasPrefix(line, []byte("* ")) {
-			continue
-		}
-
-		if !linkRegex.Match(line) {
-			t.Errorf("line %d: missing issue or pull-request link:\n\t%q", nr, string(line))
-		}
-	}
-
-}
diff --git a/handler/transport/backend_test.go b/handler/transport/backend_test.go
index 0017d2be..5ec70d5c 100644
--- a/handler/transport/backend_test.go
+++ b/handler/transport/backend_test.go
@@ -197,7 +197,7 @@ func TestBackend_RoundTrip_Validation(t *testing.T) {
 			http.MethodPost,
 			"/get",
 			"backend validation error",
-			"'POST /get': Path doesn't support the HTTP method",
+			"request validation: 'POST /get': Path doesn't support the HTTP method",
 		},
 		{
 			"invalid request, IgnoreRequestViolations",
@@ -205,7 +205,7 @@ func TestBackend_RoundTrip_Validation(t *testing.T) {
 			http.MethodPost,
 			"/get",
 			"",
-			"'POST /get': Path doesn't support the HTTP method",
+			"request validation: 'POST /get': Path doesn't support the HTTP method",
 		},
 		{
 			"invalid response",
@@ -213,7 +213,7 @@ func TestBackend_RoundTrip_Validation(t *testing.T) {
 			http.MethodGet,
 			"/get?404",
 			"backend validation error",
-			"status is not supported",
+			"response validation: status is not supported",
 		},
 		{
 			"invalid response, IgnoreResponseViolations",
@@ -221,11 +221,11 @@ func TestBackend_RoundTrip_Validation(t *testing.T) {
 			http.MethodGet,
 			"/get?404",
 			"",
-			"status is not supported",
+			"response validation: status is not supported",
 		},
 	}
 
-	logger, hook := test.NewLogger()
+	logger, hook := logrustest.NewNullLogger()
 	log := logger.WithContext(context.Background())
 
 	for _, tt := range tests {
@@ -260,15 +260,13 @@ func TestBackend_RoundTrip_Validation(t *testing.T) {
 			entry := hook.LastEntry()
 			if tt.expectedLogMessage != "" {
 				if data, ok := entry.Data["validation"]; ok {
-					for _, errStr := range data.([]string) {
-						if errStr != tt.expectedLogMessage {
-							subT.Errorf("\nwant:\t%s\ngot:\t%v", tt.expectedLogMessage, errStr)
+					for _, err := range data.([]string) {
+						if err == tt.expectedLogMessage {
 							return
 						}
-						return
 					}
-					for _, errStr := range data.([]string) {
-						subT.Log(errStr)
+					for _, err := range data.([]string) {
+						subT.Log(err)
 					}
 				}
 				subT.Errorf("expected matching validation error logs:\n\t%s\n\tgot: nothing", tt.expectedLogMessage)
diff --git a/handler/validation/openapi_test.go b/handler/validation/openapi_test.go
index 9540d9e7..ead7d2e1 100644
--- a/handler/validation/openapi_test.go
+++ b/handler/validation/openapi_test.go
@@ -43,7 +43,7 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) {
 		}
 	}))
 
-	log, hook := test.NewLogger()
+	log, hook := logrustest.NewNullLogger()
 	logger := log.WithContext(context.Background())
 	beConf := &config.Backend{
 		Remain: body.New(&hcl.BodyContent{Attributes: hcl.Attributes{
@@ -69,10 +69,10 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) {
 		wantBody   bool
 		wantErrLog string
 	}{
-		{"GET without required query", "/a?b", nil, false, "backend validation error: Parameter 'b' in query has an error: must have a value: must have a value"},
+		{"GET without required query", "/a?b", nil, false, "request validation: Parameter 'b' in query has an error: must have a value: must have a value"},
 		{"GET with required query", "/a?b=value", nil, false, ""},
 		{"GET with required path", "/a/value", nil, false, ""},
-		{"GET with required path missing", "/a//", nil, false, "backend validation error: Parameter 'b' in query has an error: must have a value: must have a value"},
+		{"GET with required path missing", "/a//", nil, false, "request validation: Parameter 'b' in query has an error: must have a value: must have a value"},
 		{"GET with optional query", "/b", nil, false, ""},
 		{"GET with optional path param", "/b/a", nil, false, ""},
 		{"GET with required json body", "/json", strings.NewReader(`["hans", "wurst"]`), true, ""},
@@ -87,23 +87,32 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) {
 			}
 
 			hook.Reset()
-			var res *http.Response
-			res, err = backend.RoundTrip(req)
+			res, err := backend.RoundTrip(req)
 			if err != nil && tt.wantErrLog == "" {
-				t.Error(err)
-				return
+				helper.Must(err)
 			}
 
 			if tt.wantErrLog != "" {
-				entry := hook.LastEntry()
-				if entry.Message != tt.wantErrLog {
-					t.Errorf("Expected error log:\nwant:\t%q\ngot:\t%s", tt.wantErrLog, entry.Message)
+				var found bool
+				for _, entry := range hook.Entries {
+					if valEntry, ok := entry.Data["validation"]; ok {
+						if list, ok := valEntry.([]string); ok {
+							for _, valMsg := range list {
+								if valMsg == tt.wantErrLog {
+									found = true
+									break
+								}
+							}
+						}
+					}
+				}
+				if !found {
+					t.Errorf("Expected error log: %q", tt.wantErrLog)
 				}
 			}
 
 			if tt.wantBody {
-				var n int64
-				n, err = io.Copy(ioutil.Discard, res.Body)
+				n, err := io.Copy(ioutil.Discard, res.Body)
 				if err != nil {
 					t.Error(err)
 				}
@@ -120,92 +129,3 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) {
 		})
 	}
 }
-
-func TestOpenAPIValidator_RelativeServerURL(t *testing.T) {
-	helper := test.New(t)
-
-	log, hook := logrustest.NewNullLogger()
-	logger := log.WithContext(context.Background())
-	beConf := &config.Backend{
-		Remain: body.New(&hcl.BodyContent{Attributes: hcl.Attributes{
-			"origin": &hcl.Attribute{
-				Name: "origin",
-				Expr: hcltest.MockExprLiteral(cty.StringVal("https://httpbin.org")),
-			},
-		}}),
-		OpenAPI: &config.OpenAPI{
-			File: filepath.Join("testdata/backend_02_openapi.yaml"),
-		},
-	}
-	openAPI, err := validation.NewOpenAPIOptions(beConf.OpenAPI)
-	helper.Must(err)
-
-	backend := transport.NewBackend(beConf.Remain, &transport.Config{}, &transport.BackendOptions{
-		OpenAPI: openAPI,
-	}, logger)
-
-	t.Run("relative server URL", func(t *testing.T) {
-		req := httptest.NewRequest(http.MethodGet, "https://httpbin.org/anything", nil)
-
-		hook.Reset()
-		_, err := backend.RoundTrip(req)
-		if err != nil {
-			helper.Must(err)
-		}
-
-		if t.Failed() {
-			for _, entry := range hook.Entries {
-				t.Log(entry.String())
-			}
-		}
-	})
-}
-
-func TestOpenAPIValidator_NonCanonicalServerURL(t *testing.T) {
-	helper := test.New(t)
-
-	log, hook := logrustest.NewNullLogger()
-	logger := log.WithContext(context.Background())
-	beConf := &config.Backend{
-		Remain: body.New(&hcl.BodyContent{Attributes: hcl.Attributes{
-			"origin": &hcl.Attribute{
-				Name: "origin",
-				Expr: hcltest.MockExprLiteral(cty.StringVal("https://httpbin.org")),
-			},
-		}}),
-		OpenAPI: &config.OpenAPI{
-			File: filepath.Join("testdata/backend_03_openapi.yaml"),
-		},
-	}
-	openAPI, err := validation.NewOpenAPIOptions(beConf.OpenAPI)
-	helper.Must(err)
-
-	backend := transport.NewBackend(beConf.Remain, &transport.Config{}, &transport.BackendOptions{
-		OpenAPI: openAPI,
-	}, logger)
-
-	tests := []struct {
-		name, url string
-	}{
-		{"http", "http://httpbin.org/anything"},
-		{"https", "https://httpbin.org/anything"},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			req := httptest.NewRequest(http.MethodGet, tt.url, nil)
-
-			hook.Reset()
-			_, err := backend.RoundTrip(req)
-			if err != nil {
-				helper.Must(err)
-			}
-
-			if t.Failed() {
-				for _, entry := range hook.Entries {
-					t.Log(entry.String())
-				}
-			}
-		})
-	}
-}
diff --git a/handler/validation/testdata/backend_02_openapi.yaml b/handler/validation/testdata/backend_02_openapi.yaml
deleted file mode 100644
index 7dd24e6e..00000000
--- a/handler/validation/testdata/backend_02_openapi.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-openapi: '3'
-info:
-  title: 'Couper backend validation test: relative server URL'
-  version: 'v1.2.3'
-servers:
-  - url: /
-paths:
-  /anything:
-    get:
-      responses:
-        200:
-          description: OK
diff --git a/handler/validation/testdata/backend_03_openapi.yaml b/handler/validation/testdata/backend_03_openapi.yaml
deleted file mode 100644
index 94f8a419..00000000
--- a/handler/validation/testdata/backend_03_openapi.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-openapi: '3'
-info:
-  title: 'Couper backend validation test: non-canonical server URL'
-  version: 'v1.2.3'
-servers:
-  - url: https://httpbin.org/
-  - url: http://httpbin.org/
-paths:
-  /anything:
-    get:
-      responses:
-        200:
-          description: OK
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 6ea0a254..e88f5ffc 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -18,7 +18,6 @@ import (
 	"reflect"
 	"strconv"
 	"strings"
-	"sync"
 	"testing"
 	"time"
 
@@ -1876,48 +1875,6 @@ func TestHTTPServer_Endpoint_Evaluation_Inheritance_Backend_Block(t *testing.T)
 	}
 }
 
-func TestOpenAPIValidateConcurrentRequests(t *testing.T) {
-	helper := test.New(t)
-	client := newClient()
-
-	shutdown, _ := newCouper("testdata/integration/validation/01_couper.hcl", test.New(t))
-	defer shutdown()
-
-	req1, err := http.NewRequest(http.MethodGet, "http://example.com:8080/anything", nil)
-	helper.Must(err)
-	req2, err := http.NewRequest(http.MethodGet, "http://example.com:8080/pdf", nil)
-	helper.Must(err)
-
-	var res1, res2 *http.Response
-	var err1, err2 error
-	waitCh := make(chan struct{})
-	wg := sync.WaitGroup{}
-	wg.Add(2)
-	go func() {
-		defer wg.Done()
-		<-waitCh // blocks
-		res1, err1 = client.Do(req1)
-	}()
-	go func() {
-		defer wg.Done()
-		<-waitCh // blocks
-		res2, err2 = client.Do(req2)
-	}()
-
-	close(waitCh) // triggers reqs
-	wg.Wait()
-
-	helper.Must(err1)
-	helper.Must(err2)
-
-	if res1.StatusCode != 200 {
-		t.Errorf("Expected status %d for response1; got: %d", 200, res1.StatusCode)
-	}
-	if res2.StatusCode != 502 {
-		t.Errorf("Expected status %d for response2; got: %d", 502, res2.StatusCode)
-	}
-}
-
 func TestConfigBodyContent(t *testing.T) {
 	helper := test.New(t)
 	client := newClient()
diff --git a/server/testdata/integration/validation/01_couper.hcl b/server/testdata/integration/validation/01_couper.hcl
deleted file mode 100644
index 52e10d84..00000000
--- a/server/testdata/integration/validation/01_couper.hcl
+++ /dev/null
@@ -1,18 +0,0 @@
-server "concurrent-requests" {
-  api {
-    endpoint "/**" {
-      proxy {
-        backend = "test-be"
-      }
-    }
-  }
-}
-
-definitions {
-  backend "test-be" {
-    origin = env.COUPER_TEST_BACKEND_ADDR
-    openapi {
-      file = "01_schema.yaml"
-    }
-  }
-}
diff --git a/server/testdata/integration/validation/01_schema.yaml b/server/testdata/integration/validation/01_schema.yaml
deleted file mode 100644
index b81dcd4c..00000000
--- a/server/testdata/integration/validation/01_schema.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-openapi: '3'
-info:
-  title: 'Couper backend validation test'
-  version: 'v1.2.3'
-paths:
-  /anything:
-    get:
-      responses:
-        200:
-          description: OK
-          schema:
-            type: object
-  /pdf:
-    get:
-      responses:
-        555:
-          description: OK