Repository: avenga/couper
Pull request #103 (2021-01-26 10:06:41)

Add multi API support


Patch:
diff --git a/config/api.go b/config/api.go
index 2e650633..f3961893 100644
--- a/config/api.go
+++ b/config/api.go
@@ -5,9 +5,11 @@ import (
 	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
-var _ Inline = &Api{}
+var _ Inline = &API{}
 
-type Api struct {
+type APIs []*API
+
+type API struct {
 	AccessControl        []string  `hcl:"access_control,optional"`
 	CORS                 *CORS     `hcl:"cors,block"`
 	Backend              string    `hcl:"backend,optional"`
@@ -15,18 +17,18 @@ type Api struct {
 	DisableAccessControl []string  `hcl:"disable_access_control,optional"`
 	Endpoints            Endpoints `hcl:"endpoint,block"`
 	ErrorFile            string    `hcl:"error_file,optional"`
-	Remain               hcl.Body  `hcl:",remain" json:"-"`
+	Remain               hcl.Body  `hcl:",remain"`
 }
 
-func (a Api) Body() hcl.Body {
+func (a API) Body() hcl.Body {
 	return a.Remain
 }
 
-func (a Api) Reference() string {
+func (a API) Reference() string {
 	return a.Backend
 }
 
-func (a Api) Schema(inline bool) *hcl.BodySchema {
+func (a API) Schema(inline bool) *hcl.BodySchema {
 	if !inline {
 		schema, _ := gohcl.ImpliedBodySchema(a)
 		return schema
diff --git a/config/configload/load.go b/config/configload/load.go
index 017cd08e..586e9ea4 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -1,6 +1,7 @@
 package configload
 
 import (
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"path/filepath"
@@ -125,7 +126,7 @@ func LoadConfig(body hcl.Body, src []byte) (*config.CouperFile, error) {
 		srv.Remain = MergeBodies(serverBodies)
 
 		// api block(s)
-		for _, apiBlock := range []*config.Api{srv.API} {
+		for _, apiBlock := range srv.APIs {
 			if apiBlock == nil {
 				continue
 			}
@@ -144,7 +145,7 @@ func LoadConfig(body hcl.Body, src []byte) (*config.CouperFile, error) {
 		}
 
 		// standalone endpoints
-		if err := refineEndpoints(backends, nil, srv.Endpoints); err != nil {
+		if err := refineEndpoints(backends, serverBodies, srv.Endpoints); err != nil {
 			return nil, err
 		}
 	}
@@ -299,10 +300,14 @@ func validateOrigin(merged hcl.Body) error {
 
 	_, ok := content.Attributes["origin"]
 	if !ok {
+		err := errors.New("missing backend.origin attribute")
 		bodyRange := merged.MissingItemRange()
+		if bodyRange.Filename == "<empty>" {
+			return err
+		}
 		return hcl.Diagnostics{&hcl.Diagnostic{
 			Subject: &bodyRange,
-			Summary: "missing backend.origin attribute",
+			Summary: err.Error(),
 		}}
 	}
 	return nil
diff --git a/config/runtime/server.go b/config/runtime/server.go
index ccf4791e..5e81ee8b 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -53,7 +53,7 @@ const (
 	KindSPA
 )
 
-type endpointList map[*config.Endpoint]HandlerKind
+type endpointMap map[*config.Endpoint]*config.API
 
 // NewServerConfiguration sets http handler specific defaults and validates the given gateway configuration.
 // Wire up all endpoints and maps them within the returned Server.
@@ -85,7 +85,7 @@ func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerC
 		}
 	}
 
-	api := make(map[*config.Endpoint]http.Handler)
+	endpointHandler := make(map[*config.Endpoint]http.Handler)
 
 	for _, srvConf := range conf.Server {
 		serverOptions, err := server.NewServerOptions(srvConf)
@@ -130,17 +130,16 @@ func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerC
 
 		endpointsPatterns := make(map[string]bool)
 
-		for endpoint, epType := range getEndpointsList(srvConf) {
+		for endpoint, parentAPI := range newEndpointMap(srvConf) {
 			var basePath string
 			var cors *config.CORS
 			var errTpl *errors.Template
 
-			switch epType {
-			case KindAPI:
-				basePath = serverOptions.APIBasePath
-				cors = srvConf.API.CORS
-				errTpl = serverOptions.APIErrTpl
-			case KindEndpoint:
+			if parentAPI != nil {
+				basePath = serverOptions.APIBasePath[parentAPI]
+				cors = parentAPI.CORS
+				errTpl = serverOptions.APIErrTpl[parentAPI]
+			} else {
 				basePath = serverOptions.SrvBasePath
 				errTpl = serverOptions.ServerErrTpl
 			}
@@ -154,13 +153,13 @@ func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerC
 
 			// setACHandlerFn individual wrap for access_control configuration per endpoint
 			setACHandlerFn := func(protectedHandler http.Handler) {
-				ac := config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl)
-				if epType == KindAPI {
-					ac = ac.Merge(config.NewAccessControl(srvConf.API.AccessControl, srvConf.API.DisableAccessControl))
+				accessControl := config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl)
+
+				if parentAPI != nil {
+					accessControl = accessControl.Merge(config.NewAccessControl(parentAPI.AccessControl, parentAPI.DisableAccessControl))
 				}
 
-				api[endpoint] = configureProtectedHandler(accessControls, serverOptions.APIErrTpl,
-					ac,
+				endpointHandler[endpoint] = configureProtectedHandler(accessControls, errTpl, accessControl,
 					config.NewAccessControl(endpoint.AccessControl, endpoint.DisableAccessControl),
 					protectedHandler)
 			}
@@ -170,17 +169,19 @@ func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerC
 				return nil, diags
 			}
 
-			backend, err := newProxy(
-				confCtx, &backendConf, cors, log, serverOptions,
-				conf.Settings.NoProxyFromEnv, errTpl, epType,
-			)
+			kind := KindEndpoint
+			if parentAPI != nil {
+				kind = KindAPI
+			}
+			backend, err := newProxy(confCtx, &backendConf, cors, log, serverOptions,
+				conf.Settings.NoProxyFromEnv, errTpl, kind)
 			if err != nil {
 				return nil, err
 			}
 
 			setACHandlerFn(backend)
 
-			err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
+			err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, endpointHandler[endpoint], KindAPI)
 			if err != nil {
 				return nil, err
 			}
@@ -362,17 +363,17 @@ func setRoutesFromHosts(srvConf ServerConfiguration, defaultPort int, hosts []st
 	return nil
 }
 
-func getEndpointsList(srvConf *config.Server) endpointList {
-	endpoints := make(endpointList)
+func newEndpointMap(srvConf *config.Server) endpointMap {
+	endpoints := make(endpointMap)
 
-	if srvConf.API != nil {
-		for _, endpoint := range srvConf.API.Endpoints {
-			endpoints[endpoint] = KindAPI
+	for _, api := range srvConf.APIs {
+		for _, endpoint := range api.Endpoints {
+			endpoints[endpoint] = api
 		}
 	}
 
 	for _, endpoint := range srvConf.Endpoints {
-		endpoints[endpoint] = KindEndpoint
+		endpoints[endpoint] = nil
 	}
 
 	return endpoints
diff --git a/config/runtime/server/options.go b/config/runtime/server/options.go
index b4331de2..7156ad69 100644
--- a/config/runtime/server/options.go
+++ b/config/runtime/server/options.go
@@ -13,10 +13,10 @@ type Context interface {
 }
 
 type Options struct {
-	APIErrTpl    *errors.Template
+	APIErrTpl    map[*config.API]*errors.Template
 	FileErrTpl   *errors.Template
 	ServerErrTpl *errors.Template
-	APIBasePath  string
+	APIBasePath  map[*config.API]string
 	FileBasePath string
 	SPABasePath  string
 	SrvBasePath  string
@@ -25,7 +25,6 @@ type Options struct {
 
 func NewServerOptions(conf *config.Server) (*Options, error) {
 	options := &Options{
-		APIErrTpl:    errors.DefaultJSON,
 		FileErrTpl:   errors.DefaultHTML,
 		ServerErrTpl: errors.DefaultHTML,
 	}
@@ -45,15 +44,22 @@ func NewServerOptions(conf *config.Server) (*Options, error) {
 		options.FileErrTpl = tpl
 	}
 
-	if conf.API != nil {
-		options.APIBasePath = path.Join(options.SrvBasePath, conf.API.BasePath)
+	if len(conf.APIs) > 0 {
+		options.APIBasePath = make(map[*config.API]string)
+		options.APIErrTpl = make(map[*config.API]*errors.Template)
+	}
+
+	for _, api := range conf.APIs {
+		options.APIBasePath[api] = path.Join(options.SrvBasePath, api.BasePath)
 
-		if conf.API.ErrorFile != "" {
-			tpl, err := errors.NewTemplateFromFile(conf.API.ErrorFile)
+		if api.ErrorFile != "" {
+			tpl, err := errors.NewTemplateFromFile(api.ErrorFile)
 			if err != nil {
 				return nil, err
 			}
-			options.APIErrTpl = tpl
+			options.APIErrTpl[api] = tpl
+		} else {
+			options.APIErrTpl[api] = errors.DefaultJSON
 		}
 	}
 
diff --git a/config/runtime/server_validation.go b/config/runtime/server_validation.go
index 763fe567..bdb89d29 100644
--- a/config/runtime/server_validation.go
+++ b/config/runtime/server_validation.go
@@ -77,11 +77,11 @@ func validateACName(accessControls ac.Map, name, acType string) (string, error)
 	name = strings.TrimSpace(name)
 
 	if name == "" {
-		return name, fmt.Errorf("Missing a non-empty label for %q", acType)
+		return name, fmt.Errorf("access control: label required: '%s'", acType)
 	}
 
 	if _, ok := accessControls[name]; ok {
-		return name, fmt.Errorf("Label %q already exists in the ACL", name)
+		return name, fmt.Errorf("access control: '%s' already exists", name)
 	}
 
 	return name, nil
diff --git a/config/server.go b/config/server.go
index 157e9100..c97bba90 100644
--- a/config/server.go
+++ b/config/server.go
@@ -11,7 +11,7 @@ var _ Inline = &Server{}
 type Server struct {
 	AccessControl        []string  `hcl:"access_control,optional"`
 	DisableAccessControl []string  `hcl:"disable_access_control,optional"`
-	API                  *Api      `hcl:"api,block"`
+	APIs                 APIs      `hcl:"api,block"`
 	Backend              string    `hcl:"backend,optional"`
 	BasePath             string    `hcl:"base_path,optional"`
 	Endpoints            Endpoints `hcl:"endpoint,block"`
diff --git a/docs/README.md b/docs/README.md
index c4b0159d..75df7d5f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -73,7 +73,7 @@ Couper's configuration file consists of nested configuration blocks that configu
 For orientation compare the following example and the information below:
 
 ```hcl
-server "my_project" {		
+server "my_project" {
   files { ... }
   
   spa { ... }
@@ -92,7 +92,7 @@ definitions { ... }
 * `server` main configuration block
     * `files` configuration block for file serving
     * `spa` configuration block for web serving (spa assets) 
-    * `api` configuration block that bundles endpoints under a certain base path
+    * `api` configuration block that bundles endpoints under a certain base path, multiple blocks are allowed
     * `access_control` attribute that sets access control for a block context
     * `endpoint` configuration block for Couper's entry points
     * `backend` configuration block for connection to local/remote backend service(s)
@@ -219,7 +219,7 @@ It has an optional label and a `hosts` attribute. Nested blocks are `files`, `sp
 |[**`access_control`**](#access_control_attribute)|<ul><li>sets predefined `access_control` for `server` block</li><li>*example:* `access_control = ["foo"]`</li><li>&#9888; inherited</li></ul>|
 |[**`files`**](#fi) block|configures file serving|
 |[**`spa`**](#spa) block|configures web serving for spa assets|
-|[**`api`**](#api) block|configures routing and backend connection(s)|
+|[**`api`**](#api) block(s)|configures routing and backend connection(s)|
 
 
 ### The `files` block <a name="files_block"></a>
@@ -251,7 +251,6 @@ If an error occurred for api endpoints the response gets processed as json error
 | Name | Description                           |
 |:-------------------|:---------------------------------------|
 |context|`server` block|
-|*label*|&#9888; mandatory, if there is more than one `api` block|
 | `base_path`|<ul><li>optional</li><li>*example:* `base_path = "/api" `</li></ul> |
 | `error_file` | <ul><li>location of the error template file</li><li>*example:* `error_file = "./my_error_body.json" `</li></ul> |
 |[**`access_control`**](#access_control_attribute)|<ul><li>sets predefined `access_control` for `api` block context</li><li>&#9888; inherited by all endpoints in `api` block context</li></ul>|
diff --git a/handler/proxy.go b/handler/proxy.go
index e155f659..157707f2 100644
--- a/handler/proxy.go
+++ b/handler/proxy.go
@@ -5,6 +5,7 @@ import (
 	"compress/gzip"
 	"context"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"io"
 	"math"
@@ -369,9 +370,13 @@ func (p *Proxy) Director(req *http.Request) error {
 		path = pathVal
 	}
 
+	if origin == "" {
+		return errors.New("proxy: origin not set")
+	}
+
 	originURL, err := url.Parse(origin)
 	if err != nil {
-		return err
+		return fmt.Errorf("proxy: parse origin: %w", err)
 	}
 
 	req.URL.Host = originURL.Host
@@ -410,7 +415,7 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
 		attrCtxDel = attrDelReqHeaders
 		attrCtxSet = attrSetReqHeaders
 		bereq      *http.Request
-		headerCtx  http.Header
+		headerCtx  = req.Header
 	)
 
 	if beresp != nil {
@@ -421,16 +426,6 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
 		headerCtx = beresp.Header
 
 		defer p.setCorsRespHeaders(headerCtx, req)
-	} else if req != nil {
-		headerCtx = req.Header
-
-		// Remove blacklisted headers after evaluation to
-		// be accessible within our context configuration.
-		if attrCtxSet == attrSetReqHeaders {
-			for _, key := range headerBlacklist {
-				headerCtx.Del(key)
-			}
-		}
 	}
 
 	allAttributes, attrOk := p.options.Context.(body.Attributes)
@@ -440,6 +435,14 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
 
 	evalCtx := eval.NewHTTPContext(p.evalContext, p.bufferOption, req, bereq, beresp)
 
+	// Remove blacklisted headers after evaluation to
+	// be accessible within our context configuration.
+	if attrCtxSet == attrSetReqHeaders {
+		for _, key := range headerBlacklist {
+			headerCtx.Del(key)
+		}
+	}
+
 	var modifyQuery bool
 
 	u := *req.URL
@@ -764,21 +767,6 @@ func (p *Proxy) getErrorCode(code couperErr.Code) couperErr.Code {
 	return code
 }
 
-func setHeaderFields(header http.Header, options OptionsMap) {
-	if len(options) == 0 {
-		return
-	}
-
-	for key, value := range options {
-		k := http.CanonicalHeaderKey(key)
-		if (len(value) == 0 || value[0] == "") && k != "User-Agent" {
-			header.Del(k)
-			continue
-		}
-		header[k] = value
-	}
-}
-
 func getAttribute(ctx *hcl.EvalContext, name string, body *hcl.BodyContent) string {
 	attr := body.Attributes
 	if _, ok := attr[name]; !ok {
diff --git a/server/mux.go b/server/mux.go
index 540a872f..c68c5a89 100644
--- a/server/mux.go
+++ b/server/mux.go
@@ -139,8 +139,8 @@ func (m *Mux) FindHandler(req *http.Request) http.Handler {
 		// No matches for api or free endpoints. Determine if we have entered an api basePath
 		// and handle api related errors accordingly.
 		// Otherwise look for existing files or spa fallback.
-		if srvCtxOpts != nil && isConfigured(srvCtxOpts.APIBasePath) && isAPIError(srvCtxOpts, req.URL.Path) {
-			return srvCtxOpts.APIErrTpl.ServeError(errors.APIRouteNotFound)
+		if tpl := getAPIErrorTemplate(srvCtxOpts, req.URL.Path); tpl != nil {
+			return tpl.ServeError(errors.APIRouteNotFound)
 		}
 
 		fileHandler, fileSrvCtxOpts, exist := m.hasFileResponse(req)
@@ -273,14 +273,32 @@ func unwrapServerOptions(suffix pathpattern.Suffix) *server.Options {
 	return unwrapServerOptions(suffix.Node.Suffixes[len(suffix.Node.Suffixes)-1]) // FIXME: check other more explicit suffixes?
 }
 
+func getAPIErrorTemplate(srvOptions *server.Options, reqPath string) *errors.Template {
+	if srvOptions == nil {
+		return nil
+	}
+
+	for api, path := range srvOptions.APIBasePath {
+		if !isConfigured(path) {
+			continue
+		}
+
+		if isAPIError(srvOptions, path, reqPath) {
+			return srvOptions.APIErrTpl[api]
+		}
+	}
+
+	return nil
+}
+
 // isAPIError checks the path w/ and w/o the
 // trailing slash against the request path.
-func isAPIError(srvOpts *server.Options, reqPath string) bool {
+func isAPIError(srvOpts *server.Options, apiPath, reqPath string) bool {
 	if srvOpts == nil {
 		return false
 	}
-	p1 := srvOpts.APIBasePath
-	p2 := srvOpts.APIBasePath
+	p1 := apiPath
+	p2 := apiPath
 
 	if p1 != "/" && !strings.HasSuffix(p1, "/") {
 		p1 += "/"
@@ -290,10 +308,10 @@ func isAPIError(srvOpts *server.Options, reqPath string) bool {
 	}
 
 	if strings.HasPrefix(reqPath, p1) || reqPath == p2 {
-		if isConfigured(srvOpts.FileBasePath) && srvOpts.APIBasePath == srvOpts.FileBasePath {
+		if isConfigured(srvOpts.FileBasePath) && apiPath == srvOpts.FileBasePath {
 			return false
 		}
-		if isConfigured(srvOpts.SPABasePath) && srvOpts.APIBasePath == srvOpts.SPABasePath {
+		if isConfigured(srvOpts.SPABasePath) && apiPath == srvOpts.SPABasePath {
 			return false
 		}
 


Test patch:
diff --git a/config/runtime/server_internal_test.go b/config/runtime/server_internal_test.go
index 02eed1f9..810b1519 100644
--- a/config/runtime/server_internal_test.go
+++ b/config/runtime/server_internal_test.go
@@ -5,9 +5,6 @@ import (
 	"testing"
 
 	"github.com/avenga/couper/config"
-	"github.com/avenga/couper/internal/seetie"
-	"github.com/hashicorp/hcl/v2"
-	"github.com/hashicorp/hcl/v2/hcltest"
 )
 
 func TestServer_isUnique(t *testing.T) {
@@ -170,28 +167,22 @@ func TestServer_splitWildcardHostPort(t *testing.T) {
 }
 
 func TestServer_getEndpointsList(t *testing.T) {
-	getHCLBody := func(in string) hcl.Body {
-		return hcltest.MockBody(&hcl.BodyContent{
-			Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
-				"path": hcltest.MockExprLiteral(seetie.GoToValue(in)),
-			}),
-		})
-	}
-
 	srvConf := &config.Server{
-		API: &config.Api{
-			Endpoints: []*config.Endpoint{
-				{Remain: getHCLBody("/api/1")},
-				{Remain: getHCLBody("/api/2")},
+		APIs: []*config.API{
+			{
+				Endpoints: []*config.Endpoint{
+					{Pattern: "/api/1"},
+					{Pattern: "/api/2"},
+				},
 			},
 		},
 		Endpoints: []*config.Endpoint{
-			{Remain: getHCLBody("/free/1")},
-			{Remain: getHCLBody("/free/2")},
+			{Pattern: "/free/1"},
+			{Pattern: "/free/2"},
 		},
 	}
 
-	endpoints := getEndpointsList(srvConf)
+	endpoints := newEndpointMap(srvConf)
 	if l := len(endpoints); l != 4 {
 		t.Fatalf("Expected 4 endpointes, given %d", l)
 	}
@@ -203,19 +194,19 @@ func TestServer_getEndpointsList(t *testing.T) {
 		"/free/2": KindEndpoint,
 	}
 
-	for e, kind := range endpoints {
-		a, _ := e.Remain.JustAttributes()
-		v, _ := a["path"].Expr.Value(nil)
-		path := seetie.ValueToString(v)
-
-		if v, ok := checks[path]; !ok || v != kind {
-			t.Fatalf("Missing an endpoint for %s", path)
+	for pattern, kind := range checks {
+		var exist bool
+		for endpoint, parent := range endpoints {
+			if endpoint.Pattern == pattern {
+				if kind == KindAPI && parent == nil {
+					t.Errorf("Expected an api endpoint for path pattern: %q", pattern)
+				}
+				exist = true
+				break
+			}
+		}
+		if !exist {
+			t.Errorf("Expected an endpoint for path pattern: %q", pattern)
 		}
-
-		delete(checks, path)
-	}
-
-	if l := len(checks); l != 0 {
-		t.Fatalf("Expected 0 checks, given %d", l)
 	}
 }
diff --git a/handler/openapi_validator_test.go b/handler/openapi_validator_test.go
index ce516a99..85f83d4c 100644
--- a/handler/openapi_validator_test.go
+++ b/handler/openapi_validator_test.go
@@ -18,7 +18,6 @@ import (
 	logrustest "github.com/sirupsen/logrus/hooks/test"
 
 	"github.com/avenga/couper/config"
-	"github.com/avenga/couper/config/runtime/server"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
@@ -64,7 +63,7 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) {
 	proxyOpts, err := handler.NewProxyOptions(beConf, &handler.CORSOptions{}, config.DefaultSettings.NoProxyFromEnv, errors.DefaultJSON, "api")
 	helper.Must(err)
 
-	backend, err := handler.NewProxy(proxyOpts, log.WithContext(context.Background()), &server.Options{APIErrTpl: errors.DefaultJSON}, eval.NewENVContext(nil))
+	backend, err := handler.NewProxy(proxyOpts, log.WithContext(context.Background()), nil, eval.NewENVContext(nil))
 	helper.Must(err)
 
 	tests := []struct {
diff --git a/handler/proxy_test.go b/handler/proxy_test.go
index 03ff4efd..bd158827 100644
--- a/handler/proxy_test.go
+++ b/handler/proxy_test.go
@@ -460,7 +460,7 @@ func TestProxy_ServeHTTP_Validation(t *testing.T) {
 		},
 	}
 
-	srvOpts := &server.Options{APIErrTpl: errors.DefaultJSON}
+	srvOpts := &server.Options{ServerName: "test"}
 
 	logger, hook := logrustest.NewNullLogger()
 
@@ -651,16 +651,13 @@ func TestProxy_ServeHTTP_Eval(t *testing.T) {
 			}
 
 			p, err := handler.NewProxy(&handler.ProxyOptions{
+				ErrorTemplate:    errors.DefaultJSON,
 				RequestBodyLimit: 10,
 				Context: configload.MergeBodies([]hcl.Body{
 					test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 					remain.Inline,
 				}),
-			}, log.WithContext(context.Background()),
-				&server.Options{
-					APIErrTpl: errors.DefaultJSON,
-				},
-				baseCtx)
+			}, log.WithContext(context.Background()), nil, baseCtx)
 
 			if err != nil {
 				t.Fatal(err)
@@ -975,10 +972,11 @@ func TestProxy_SetRoundtripContext_Null_Eval(t *testing.T) {
 			evalCtx := eval.NewENVContext(nil)
 
 			proxy, err := handler.NewProxy(&handler.ProxyOptions{
+				ErrorTemplate: errors.DefaultJSON,
 				Context: configload.MergeBodies([]hcl.Body{test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 					helper.NewProxyContext(tc.remain)}),
 				RequestBodyLimit: 64,
-			}, log.WithContext(context.Background()), &server.Options{APIErrTpl: errors.DefaultJSON}, evalCtx)
+			}, log.WithContext(context.Background()), nil, evalCtx)
 			h.Must(err)
 
 			req := httptest.NewRequest(http.MethodGet, "http://localhost/", bytes.NewReader(clientPayload))
@@ -1070,11 +1068,12 @@ func TestProxy_BufferingOptions(t *testing.T) {
 			evalCtx := eval.NewENVContext(nil)
 
 			proxy, err := handler.NewProxy(&handler.ProxyOptions{
-				OpenAPI: tc.apiOptions,
+				ErrorTemplate: errors.DefaultJSON,
+				OpenAPI:       tc.apiOptions,
 				Context: configload.MergeBodies([]hcl.Body{test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 					helper.NewProxyContext(tc.remain)}),
 				RequestBodyLimit: 64,
-			}, log.WithContext(context.Background()), &server.Options{APIErrTpl: errors.DefaultJSON}, evalCtx)
+			}, log.WithContext(context.Background()), nil, evalCtx)
 			h.Must(err)
 
 			configuredOption := reflect.ValueOf(proxy).Elem().FieldByName("bufferOption") // private field: ro
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 8bd35414..845f3520 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -16,6 +16,7 @@ import (
 	"path"
 	"path/filepath"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
@@ -1006,7 +1007,8 @@ func TestHTTPServer_Endpoint_Evaluation_Inheritance_Backend_Block(t *testing.T)
 
 	shutdown, _ := newCouper("testdata/integration/endpoint_eval/08_couper.hcl", test.New(t))
 
-	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/"+
+		strings.Replace(testBackend.Addr(), "http://", "", 1), nil)
 	helper.Must(err)
 
 	res, err := client.Do(req)
@@ -1106,6 +1108,74 @@ func TestConfigBodyContentBackends(t *testing.T) {
 	shutdown()
 }
 
+func TestConfigBodyContentAccessControl(t *testing.T) {
+	client := newClient()
+
+	shutdown, _ := newCouper("testdata/integration/config/03_couper.hcl", test.New(t))
+
+	type testCase struct {
+		path   string
+		header http.Header
+		status int
+	}
+
+	for _, tc := range []testCase{
+		{"/v1", http.Header{"Auth": []string{"ba1"}}, http.StatusOK},
+		// TODO: Can a disabled auth being enabled again?
+		//{"/v1", http.Header{"Authorization": []string{"Basic Og=="}, "Auth": []string{"ba1"}}, http.StatusOK},
+		//{"/v1", http.Header{"Auth": []string{}}, http.StatusUnauthorized},
+		{"/v2", http.Header{"Authorization": []string{"Basic Og=="}, "Auth": []string{"ba1", "ba2"}}, http.StatusOK}, // minimum ':'
+		{"/v2", http.Header{}, http.StatusUnauthorized},
+		{"/v3", http.Header{}, http.StatusOK},
+		{"/status", http.Header{}, http.StatusOK},
+		{"/superadmin", http.Header{"Authorization": []string{"Basic Og=="}, "Auth": []string{"ba1", "ba4"}}, http.StatusOK},
+		{"/superadmin", http.Header{}, http.StatusUnauthorized},
+	} {
+		t.Run(tc.path[1:], func(subT *testing.T) {
+			helper := test.New(subT)
+			req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil)
+			helper.Must(err)
+
+			if val := tc.header.Get("Authorization"); val != "" {
+				req.Header.Set("Authorization", val)
+			}
+
+			res, err := client.Do(req)
+			helper.Must(err)
+
+			if res.StatusCode != tc.status {
+				t.Errorf("%q: expected Status %d, got: %d", tc.path, tc.status, res.StatusCode)
+				return
+			}
+
+			if ct := res.Header.Get("Content-Type"); ct != "application/json" {
+				t.Errorf("%q: unexpected content-type: %q", tc.path, ct)
+				return
+			}
+
+			b, err := ioutil.ReadAll(res.Body)
+
+			type payload struct {
+				Headers http.Header
+			}
+			var p payload
+			helper.Must(json.Unmarshal(b, &p))
+
+			for k, v := range tc.header {
+				if _, ok := p.Headers[k]; !ok {
+					t.Errorf("Expected header %q, got nothing", k)
+					break
+				}
+				if !reflect.DeepEqual(p.Headers[k], v) {
+					t.Errorf("Expected header %q value: %v, got: %v", k, v, p.Headers[k])
+				}
+			}
+		})
+	}
+
+	shutdown()
+}
+
 func TestWrapperHiJack_WebsocketUpgrade(t *testing.T) {
 	helper := test.New(t)
 	shutdown, _ := newCouper("testdata/integration/api/04_couper.hcl", test.New(t))
@@ -1149,3 +1219,57 @@ func TestWrapperHiJack_WebsocketUpgrade(t *testing.T) {
 		t.Errorf("Expected pong answer, got: %q", string(p))
 	}
 }
+
+func TestHTTPServer_MultiAPI(t *testing.T) {
+	client := newClient()
+
+	type expectation struct {
+		Path string
+	}
+
+	type testCase struct {
+		path string
+		exp  expectation
+	}
+
+	for _, tc := range []testCase{
+		{"/xxx", expectation{
+			Path: "/xxx",
+		}},
+		{"/yyy", expectation{
+			Path: "/yyy",
+		}},
+		{"/zzz", expectation{
+			Path: "/zzz",
+		}},
+	} {
+		shutdown, _ := newCouper("testdata/integration/api/05_couper.hcl", test.New(t))
+
+		t.Run(tc.path, func(subT *testing.T) {
+			helper := test.New(subT)
+
+			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
+			helper.Must(err)
+
+			res, err := client.Do(req)
+			helper.Must(err)
+
+			resBytes, err := ioutil.ReadAll(res.Body)
+			helper.Must(err)
+
+			_ = res.Body.Close()
+
+			var jsonResult expectation
+			err = json.Unmarshal(resBytes, &jsonResult)
+			if err != nil {
+				t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
+			}
+
+			if !reflect.DeepEqual(jsonResult, tc.exp) {
+				t.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes))
+			}
+		})
+
+		shutdown()
+	}
+}
diff --git a/server/testdata/integration/api/05_couper.hcl b/server/testdata/integration/api/05_couper.hcl
new file mode 100644
index 00000000..a190fdf4
--- /dev/null
+++ b/server/testdata/integration/api/05_couper.hcl
@@ -0,0 +1,25 @@
+server "multi-api" {
+  api {
+    endpoint "/xxx" {
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+
+  api {
+    endpoint "/yyy" {
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+
+  api {
+    endpoint "/zzz" {
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+}
diff --git a/server/testdata/integration/config/03_couper.hcl b/server/testdata/integration/config/03_couper.hcl
new file mode 100644
index 00000000..a53f2502
--- /dev/null
+++ b/server/testdata/integration/config/03_couper.hcl
@@ -0,0 +1,70 @@
+server "acs" {
+  access_control = ["ba1"]
+  backend = "test"
+  error_file = "../api_error.json"
+  api {
+    base_path = "/v1"
+    disable_access_control = ["ba1"]
+    endpoint "/**" {
+      # access_control = ["ba1"] # not possible atm TODO: spec
+      backend "test" {
+        set_request_headers = {
+          auth = ["ba1"]
+        }
+      }
+    }
+  }
+
+  api {
+    base_path = "/v2"
+    access_control = ["ba2"]
+    endpoint "/**" {
+      backend "test" {
+        set_request_headers = {
+          auth = ["ba1", "ba2"]
+        }
+      }
+    }
+  }
+
+  api {
+    base_path = "/v3"
+    access_control = ["ba2"]
+    endpoint "/**" {
+      access_control = ["ba3"]
+      disable_access_control = ["ba1", "ba2", "ba3"]
+    }
+  }
+
+  endpoint "/status" {
+    disable_access_control = ["ba1"]
+  }
+
+  endpoint "/superadmin" {
+    access_control = ["ba4"]
+    backend "test" {
+      set_request_headers = {
+        auth = ["ba1", "ba4"]
+      }
+    }
+  }
+}
+
+definitions {
+  basic_auth "ba1" {}
+  basic_auth "ba2" {}
+  basic_auth "ba3" {}
+  basic_auth "ba4" {}
+
+  backend "test" {
+    origin = env.COUPER_TEST_BACKEND_ADDR
+    path = "/anything"
+    set_request_headers = {
+      Authorization: req.headers.authorization
+    }
+  }
+}
+
+settings {
+  no_proxy_from_env = true
+}
diff --git a/server/testdata/integration/endpoint_eval/08_couper.hcl b/server/testdata/integration/endpoint_eval/08_couper.hcl
index 75a93862..706934cc 100644
--- a/server/testdata/integration/endpoint_eval/08_couper.hcl
+++ b/server/testdata/integration/endpoint_eval/08_couper.hcl
@@ -11,7 +11,7 @@ server "protected" {
       }
     }
 
-    endpoint "/" {
+    endpoint "/{origin}" {
       path = "/set/by/endpoint/unset/by/backend"
       backend "anything" {
         path = "/anything"