Repository: avenga/couper
Pull request #161 (2021-03-29 08:05:36)

Fix muxing


Patch:
diff --git a/command/run.go b/command/run.go
index 498c1e0d..88474e8e 100644
--- a/command/run.go
+++ b/command/run.go
@@ -49,12 +49,12 @@ func (r Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry) e
 	env.Decode(&timings)
 
 	// logEntry has still the 'daemon' type which can be used for config related load errors.
-	srvMux, err := runtime.NewServerConfiguration(config, logEntry, cache.New(logEntry, r.context.Done()))
+	srvConf, err := runtime.NewServerConfiguration(config, logEntry, cache.New(logEntry, r.context.Done()))
 	if err != nil {
 		return err
 	}
 
-	serverList, listenCmdShutdown := server.NewServerList(r.context, config.Context, logEntry.Logger, config.Settings, &timings, srvMux)
+	serverList, listenCmdShutdown := server.NewServerList(r.context, config.Context, logEntry.Logger, config.Settings, &timings, srvConf)
 	for _, srv := range serverList {
 		srv.Listen()
 	}
diff --git a/config/runtime/mux.go b/config/runtime/mux.go
index 34b80b46..a45ea89d 100644
--- a/config/runtime/mux.go
+++ b/config/runtime/mux.go
@@ -3,27 +3,20 @@ package runtime
 import (
 	"net/http"
 
-	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/config/runtime/server"
 )
 
 type MuxOptions struct {
 	EndpointRoutes map[string]http.Handler
 	FileRoutes     map[string]http.Handler
 	SPARoutes      map[string]http.Handler
-	ErrorTpl       *errors.Template
-	Hosts          hosts
+	ServerOptions  *server.Options
 }
 
-func NewMuxOptions(errorTpl *errors.Template, hostsMap hosts) *MuxOptions {
-	if hostsMap == nil {
-		hostsMap = make(hosts)
-	}
-
+func NewMuxOptions() *MuxOptions {
 	return &MuxOptions{
 		EndpointRoutes: make(map[string]http.Handler),
 		FileRoutes:     make(map[string]http.Handler),
 		SPARoutes:      make(map[string]http.Handler),
-		ErrorTpl:       errorTpl,
-		Hosts:          hostsMap,
 	}
 }
diff --git a/config/runtime/server.go b/config/runtime/server.go
index b9c57821..a647184f 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -15,7 +15,6 @@ import (
 	"time"
 
 	"github.com/docker/go-units"
-	"github.com/getkin/kin-openapi/pathpattern"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
 	"github.com/sirupsen/logrus"
@@ -36,67 +35,78 @@ import (
 	"github.com/avenga/couper/utils"
 )
 
+const (
+	api HandlerKind = iota
+	endpoint
+	files
+	spa
+)
+
 var DefaultBackendConf = &config.Backend{
 	ConnectTimeout: "10s",
 	TTFBTimeout:    "60s",
 	Timeout:        "300s",
 }
 
-type Port int
+type (
+	Port                int
+	Hosts               map[string]*MuxOptions
+	Ports               map[Port]Hosts
+	ServerConfiguration Ports
+	HandlerKind         uint8
+	endpointMap         map[*config.Endpoint]*config.API
+	endpointHandler     map[*config.Endpoint]http.Handler
+)
 
 func (p Port) String() string {
 	return strconv.Itoa(int(p))
 }
 
-type ServerConfiguration map[Port]*MuxOptions
+func GetHostPort(hostPort string) (string, int, error) {
+	var host string
+	var port int
 
-type hosts map[string]bool
-type ports map[Port]hosts
+	h, p, err := net.SplitHostPort(hostPort)
+	if err != nil {
+		return "", -1, err
+	}
 
-type HandlerKind uint8
+	host = strings.TrimRight(h, ".")
 
-const (
-	api HandlerKind = iota
-	endpoint
-	files
-	spa
-)
+	if p == "" || p == "*" {
+		port = -1
+	} else {
+		port, err = strconv.Atoi(p)
+		if err != nil {
+			return "", -1, err
+		}
+	}
 
-type endpointMap map[*config.Endpoint]*config.API
+	return host, port, nil
+}
 
 // NewServerConfiguration sets http handler specific defaults and validates the given gateway configuration.
 // Wire up all endpoints and maps them within the returned Server.
 func NewServerConfiguration(
 	conf *config.Couper, log *logrus.Entry, memStore *cache.MemoryStore,
 ) (ServerConfiguration, error) {
-	defaultPort := conf.Settings.DefaultPort
-
 	// confCtx is created to evaluate request / response related configuration errors on start.
 	noopReq := httptest.NewRequest(http.MethodGet, "https://couper.io", nil)
 	noopResp := httptest.NewRecorder().Result()
 	noopResp.Request = noopReq
 	confCtx := conf.Context.WithClientRequest(noopReq).WithBeresps(noopResp).HCLContext()
 
-	validPortMap, hostsMap, err := validatePortHosts(conf, defaultPort)
-	if err != nil {
-		return nil, err
-	}
-
 	accessControls, err := configureAccessControls(conf, confCtx)
 	if err != nil {
 		return nil, err
 	}
 
-	serverConfiguration := make(ServerConfiguration)
-	if len(validPortMap) == 0 {
-		serverConfiguration[Port(defaultPort)] = NewMuxOptions(errors.DefaultHTML, hostsMap)
-	} else {
-		for p := range validPortMap {
-			serverConfiguration[p] = NewMuxOptions(errors.DefaultHTML, hostsMap)
-		}
-	}
-
-	endpointHandlers := make(map[*config.Endpoint]http.Handler)
+	var (
+		serverConfiguration ServerConfiguration = make(ServerConfiguration)
+		defaultPort         int                 = conf.Settings.DefaultPort
+		endpointHandlers    endpointHandler     = make(endpointHandler)
+		isHostsMandatory    bool                = len(conf.Servers) > 1
+	)
 
 	for _, srvConf := range conf.Servers {
 		serverOptions, err := server.NewServerOptions(srvConf)
@@ -104,6 +114,30 @@ func NewServerConfiguration(
 			return nil, err
 		}
 
+		if err := validateHosts(srvConf.Name, srvConf.Hosts, isHostsMandatory); err != nil {
+			return nil, err
+		}
+
+		portsHosts, err := getPortsHostsList(srvConf.Hosts, defaultPort)
+		if err != nil {
+			return nil, err
+		}
+
+		for port, hosts := range portsHosts {
+			for host, muxOpts := range hosts {
+				if serverConfiguration[port] == nil {
+					serverConfiguration[port] = make(Hosts)
+				}
+
+				if _, ok := serverConfiguration[port][host]; ok {
+					return nil, fmt.Errorf("conflict: host %q already defined for port: %d", host, port)
+				}
+
+				serverConfiguration[port][host] = muxOpts
+				serverConfiguration[port][host].ServerOptions = serverOptions
+			}
+		}
+
 		var spaHandler http.Handler
 		if srvConf.Spa != nil {
 			spaHandler, err = handler.NewSpa(srvConf.Spa.BootstrapFile, serverOptions)
@@ -129,7 +163,7 @@ func NewServerConfiguration(
 				config.NewAccessControl(srvConf.Spa.AccessControl, srvConf.Spa.DisableAccessControl), h)
 
 			for _, spaPath := range srvConf.Spa.Paths {
-				err = setRoutesFromHosts(serverConfiguration, serverOptions.ServerErrTpl, defaultPort, srvConf.Hosts, path.Join(serverOptions.SPABasePath, spaPath), spaHandler, spa)
+				err = setRoutesFromHosts(serverConfiguration, portsHosts, path.Join(serverOptions.SPABasePath, spaPath), spaHandler, spa)
 				if err != nil {
 					return nil, err
 				}
@@ -137,7 +171,7 @@ func NewServerConfiguration(
 		}
 
 		if srvConf.Files != nil {
-			fileHandler, err := handler.NewFile(serverOptions.FileBasePath, srvConf.Files.DocumentRoot, serverOptions)
+			fileHandler, err := handler.NewFile(srvConf.Files.DocumentRoot, serverOptions)
 			if err != nil {
 				return nil, err
 			}
@@ -155,11 +189,11 @@ func NewServerConfiguration(
 				h = fileHandler
 			}
 
-			protectedFileHandler := configureProtectedHandler(accessControls, serverOptions.FileErrTpl,
+			protectedFileHandler := configureProtectedHandler(accessControls, serverOptions.FilesErrTpl,
 				config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl),
 				config.NewAccessControl(srvConf.Files.AccessControl, srvConf.Files.DisableAccessControl), h)
 
-			err = setRoutesFromHosts(serverConfiguration, serverOptions.ServerErrTpl, defaultPort, srvConf.Hosts, serverOptions.FileBasePath, protectedFileHandler, files)
+			err = setRoutesFromHosts(serverConfiguration, portsHosts, serverOptions.FilesBasePath, protectedFileHandler, files)
 			if err != nil {
 				return nil, err
 			}
@@ -178,12 +212,12 @@ func NewServerConfiguration(
 					return nil, err
 				}
 			} else if parentAPI != nil {
-				errTpl = serverOptions.APIErrTpl[parentAPI]
+				errTpl = serverOptions.APIErrTpls[parentAPI]
 			} else {
 				errTpl = serverOptions.ServerErrTpl
 			}
 			if parentAPI != nil {
-				basePath = serverOptions.APIBasePath[parentAPI]
+				basePath = serverOptions.APIBasePaths[parentAPI]
 
 				cors, err := middleware.NewCORSOptions(
 					getCORS(srvConf.CORS, parentAPI.CORS),
@@ -317,7 +351,7 @@ func NewServerConfiguration(
 
 			setACHandlerFn(h)
 
-			err = setRoutesFromHosts(serverConfiguration, serverOptions.ServerErrTpl, defaultPort, srvConf.Hosts, pattern, endpointHandlers[endpointConf], kind)
+			err = setRoutesFromHosts(serverConfiguration, portsHosts, pattern, endpointHandlers[endpointConf], kind)
 			if err != nil {
 				return nil, err
 			}
@@ -431,31 +465,6 @@ func getCORS(parent, curr *config.CORS) *config.CORS {
 	return curr
 }
 
-func splitWildcardHostPort(host string, configuredPort int) (string, Port, error) {
-	if !strings.Contains(host, ":") {
-		return host, Port(configuredPort), nil
-	}
-
-	ho := host
-	po := configuredPort
-	h, p, err := net.SplitHostPort(host)
-	if err != nil {
-		return "", -1, err
-	}
-	ho = h
-	if p != "" && p != "*" {
-		if !rePortCheck.MatchString(p) {
-			return "", -1, fmt.Errorf("invalid port given: %s", p)
-		}
-		po, err = strconv.Atoi(p)
-		if err != nil {
-			return "", -1, err
-		}
-	}
-
-	return ho, Port(po), nil
-}
-
 func configureAccessControls(conf *config.Couper, confCtx *hcl.EvalContext) (ac.Map, error) {
 	accessControls := make(ac.Map)
 
@@ -551,49 +560,71 @@ func configureProtectedHandler(m ac.Map, errTpl *errors.Template, parentAC, hand
 	return h
 }
 
-func setRoutesFromHosts(srvConf ServerConfiguration, srvErrHandler *errors.Template, defaultPort int, hosts []string, path string, handler http.Handler, kind HandlerKind) error {
-	hostList := hosts
-	if len(hostList) == 0 {
-		hostList = []string{"*"}
-	}
+func setRoutesFromHosts(
+	srvConf ServerConfiguration, portsHosts Ports,
+	path string, handler http.Handler, kind HandlerKind,
+) error {
+	path = utils.JoinPath("/", path)
+
+	for port, hosts := range portsHosts {
+		check := make(map[string]struct{})
+
+		for host := range hosts {
+			var routes map[string]http.Handler
+
+			switch kind {
+			case api:
+				fallthrough
+			case endpoint:
+				routes = srvConf[port][host].EndpointRoutes
+			case files:
+				routes = srvConf[port][host].FileRoutes
+			case spa:
+				routes = srvConf[port][host].SPARoutes
+			default:
+				return fmt.Errorf("unknown route kind")
+			}
 
-	for _, h := range hostList {
-		joinedPath := utils.JoinPath("/", path)
-		host, listenPort, err := splitWildcardHostPort(h, defaultPort)
-		if err != nil {
-			return err
+			key := fmt.Sprintf("%d:%s:%s\n", port, host, path)
+			if _, exist := check[key]; exist {
+				return fmt.Errorf("duplicate route found on port %q: %q", port, path)
+			}
+
+			routes[path] = handler
+			check[key] = struct{}{}
 		}
+	}
+
+	return nil
+}
+
+func getPortsHostsList(hosts []string, defaultPort int) (Ports, error) {
+	if len(hosts) == 0 {
+		hosts = append(hosts, fmt.Sprintf("*:%d", defaultPort))
+	}
+
+	portsHosts := make(Ports)
 
-		if host != "*" {
-			joinedPath = utils.JoinPath(
-				pathpattern.PathFromHost(
-					net.JoinHostPort(host, listenPort.String()), false), "/", path)
+	for _, hp := range hosts {
+		if !strings.Contains(hp, ":") {
+			hp += fmt.Sprintf(":%d", defaultPort)
 		}
 
-		srvConf[listenPort].ErrorTpl = srvErrHandler
-
-		var routes map[string]http.Handler
-
-		switch kind {
-		case api:
-			fallthrough
-		case endpoint:
-			routes = srvConf[listenPort].EndpointRoutes
-		case files:
-			routes = srvConf[listenPort].FileRoutes
-		case spa:
-			routes = srvConf[listenPort].SPARoutes
-		default:
-			return fmt.Errorf("unknown route kind")
+		host, port, err := GetHostPort(hp)
+		if err != nil {
+			return nil, err
+		} else if port == -1 {
+			port = defaultPort
 		}
 
-		if _, exist := routes[joinedPath]; exist {
-			return fmt.Errorf("duplicate route found on port %q: %q", listenPort.String(), path)
+		if portsHosts[Port(port)] == nil {
+			portsHosts[Port(port)] = make(Hosts)
 		}
 
-		routes[joinedPath] = handler
+		portsHosts[Port(port)][host] = NewMuxOptions()
 	}
-	return nil
+
+	return portsHosts, nil
 }
 
 func newEndpointMap(srvConf *config.Server) endpointMap {
diff --git a/config/runtime/server/options.go b/config/runtime/server/options.go
index 7156ad69..040a77c2 100644
--- a/config/runtime/server/options.go
+++ b/config/runtime/server/options.go
@@ -13,19 +13,19 @@ type Context interface {
 }
 
 type Options struct {
-	APIErrTpl    map[*config.API]*errors.Template
-	FileErrTpl   *errors.Template
-	ServerErrTpl *errors.Template
-	APIBasePath  map[*config.API]string
-	FileBasePath string
-	SPABasePath  string
-	SrvBasePath  string
-	ServerName   string
+	APIErrTpls    map[*config.API]*errors.Template
+	FilesErrTpl   *errors.Template
+	ServerErrTpl  *errors.Template
+	APIBasePaths  map[*config.API]string
+	FilesBasePath string
+	SPABasePath   string
+	SrvBasePath   string
+	ServerName    string
 }
 
 func NewServerOptions(conf *config.Server) (*Options, error) {
 	options := &Options{
-		FileErrTpl:   errors.DefaultHTML,
+		FilesErrTpl:  errors.DefaultHTML,
 		ServerErrTpl: errors.DefaultHTML,
 	}
 
@@ -41,25 +41,25 @@ func NewServerOptions(conf *config.Server) (*Options, error) {
 			return nil, err
 		}
 		options.ServerErrTpl = tpl
-		options.FileErrTpl = tpl
+		options.FilesErrTpl = tpl
 	}
 
 	if len(conf.APIs) > 0 {
-		options.APIBasePath = make(map[*config.API]string)
-		options.APIErrTpl = make(map[*config.API]*errors.Template)
+		options.APIBasePaths = make(map[*config.API]string)
+		options.APIErrTpls = make(map[*config.API]*errors.Template)
 	}
 
 	for _, api := range conf.APIs {
-		options.APIBasePath[api] = path.Join(options.SrvBasePath, api.BasePath)
+		options.APIBasePaths[api] = path.Join(options.SrvBasePath, api.BasePath)
 
 		if api.ErrorFile != "" {
 			tpl, err := errors.NewTemplateFromFile(api.ErrorFile)
 			if err != nil {
 				return nil, err
 			}
-			options.APIErrTpl[api] = tpl
+			options.APIErrTpls[api] = tpl
 		} else {
-			options.APIErrTpl[api] = errors.DefaultJSON
+			options.APIErrTpls[api] = errors.DefaultJSON
 		}
 	}
 
@@ -69,10 +69,10 @@ func NewServerOptions(conf *config.Server) (*Options, error) {
 			if err != nil {
 				return nil, err
 			}
-			options.FileErrTpl = tpl
+			options.FilesErrTpl = tpl
 		}
 
-		options.FileBasePath = utils.JoinPath(options.SrvBasePath, conf.Files.BasePath)
+		options.FilesBasePath = utils.JoinPath(options.SrvBasePath, conf.Files.BasePath)
 	}
 
 	if conf.Spa != nil {
diff --git a/config/runtime/server_validation.go b/config/runtime/server_validation.go
index 3cd55560..bb2e049d 100644
--- a/config/runtime/server_validation.go
+++ b/config/runtime/server_validation.go
@@ -6,71 +6,26 @@ import (
 	"strings"
 
 	ac "github.com/avenga/couper/accesscontrol"
-	"github.com/avenga/couper/config"
 )
 
 var (
 	// reValidFormat validates the format only, validating for a valid host or port is out of scope.
 	reValidFormat  = regexp.MustCompile(`^([a-z0-9.-]+|\*)(:\*|:\d{1,5})?$`)
 	reCleanPattern = regexp.MustCompile(`{([^}]+)}`)
-	rePortCheck    = regexp.MustCompile(`^(0|[1-9][0-9]{0,4})$`)
 )
 
-// validatePortHosts ensures expected host:port formats and unique hosts per port.
-// Host options:
-//	"*:<port>"					listen for all hosts on given port
-//	"*:<port(configuredPort)>	given port equals configured default port, listen for all hosts
-//	"*"							equals to "*:configuredPort"
-//	"host:*"					equals to "host:configuredPort"
-//	"host"						listen on configured default port for given host
-func validatePortHosts(conf *config.Couper, configuredPort int) (ports, hosts, error) {
-	portMap := make(ports)
-	hostMap := make(hosts)
-	isHostsMandatory := len(conf.Servers) > 1
-
-	for _, srv := range conf.Servers {
-		if isHostsMandatory && len(srv.Hosts) == 0 {
-			return nil, nil, fmt.Errorf("hosts attribute is mandatory for multiple servers: %q", srv.Name)
-		}
-
-		srvPortMap := make(ports)
-		for _, host := range srv.Hosts {
-			if !reValidFormat.MatchString(host) {
-				return nil, nil, fmt.Errorf("host format is invalid: %q", host)
-			}
-
-			ho, po, err := splitWildcardHostPort(host, configuredPort)
-			if err != nil {
-				return nil, nil, err
-			}
-
-			if _, ok := srvPortMap[po]; !ok {
-				srvPortMap[po] = make(hosts)
-			}
-
-			srvPortMap[po][ho] = true
-
-			hostMap[fmt.Sprintf("%s:%d", ho, po)] = true
-		}
-
-		// srvPortMap contains all unique host port combinations for
-		// the current server and should not exist multiple times.
-		for po, ho := range srvPortMap {
-			if _, ok := portMap[po]; !ok {
-				portMap[po] = make(hosts)
-			}
-
-			for h := range ho {
-				if _, ok := portMap[po][h]; ok {
-					return nil, nil, fmt.Errorf("conflict: host %q already defined for port: %d", h, po)
-				}
+func validateHosts(serverName string, hosts []string, isHostsMandatory bool) error {
+	if isHostsMandatory && len(hosts) == 0 {
+		return fmt.Errorf("the hosts attribute is mandatory for multiple servers: %q", serverName)
+	}
 
-				portMap[po][h] = true
-			}
+	for _, host := range hosts {
+		if !reValidFormat.MatchString(host) {
+			return fmt.Errorf("the host format is invalid: %q", host)
 		}
 	}
 
-	return portMap, hostMap, nil
+	return nil
 }
 
 func validateACName(accessControls ac.Map, name, acType string) (string, error) {
diff --git a/handler/file.go b/handler/file.go
index 25bf462f..b4e5a3af 100644
--- a/handler/file.go
+++ b/handler/file.go
@@ -31,7 +31,7 @@ type File struct {
 	srvOptions *server.Options
 }
 
-func NewFile(basePath, docRoot string, srvOpts *server.Options) (*File, error) {
+func NewFile(docRoot string, srvOpts *server.Options) (*File, error) {
 	dir, err := filepath.Abs(docRoot)
 	if err != nil {
 		return nil, err
@@ -54,7 +54,7 @@ func NewFile(basePath, docRoot string, srvOpts *server.Options) (*File, error) {
 	}
 
 	f := &File{
-		basePath:   basePath,
+		basePath:   srvOpts.FilesBasePath,
 		srvOptions: srvOpts,
 		rootDir:    http.Dir(dir),
 	}
@@ -72,7 +72,7 @@ func (f *File) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 	file, info, err := f.openDocRootFile(reqPath)
 	if err != nil {
-		f.srvOptions.FileErrTpl.ServeError(errors.FilesRouteNotFound).ServeHTTP(rw, req)
+		f.srvOptions.FilesErrTpl.ServeError(errors.FilesRouteNotFound).ServeHTTP(rw, req)
 		return
 	}
 	defer file.Close()
@@ -96,7 +96,7 @@ func (f *File) serveDirectory(reqPath string, rw http.ResponseWriter, req *http.
 
 	file, info, err := f.openDocRootFile(reqPath)
 	if err != nil || info.IsDir() {
-		f.srvOptions.FileErrTpl.ServeError(errors.FilesRouteNotFound).ServeHTTP(rw, req)
+		f.srvOptions.FilesErrTpl.ServeError(errors.FilesRouteNotFound).ServeHTTP(rw, req)
 		return
 	}
 	defer file.Close()
@@ -160,7 +160,7 @@ func (f *File) removeBasePath(reqPath string) string {
 }
 
 func (f *File) Template() *errors.Template {
-	return f.srvOptions.FileErrTpl
+	return f.srvOptions.FilesErrTpl
 }
 
 func (f *File) Options() *server.Options {
diff --git a/server/http.go b/server/http.go
index 4fb05ed8..2c125379 100644
--- a/server/http.go
+++ b/server/http.go
@@ -16,12 +16,15 @@ import (
 	"github.com/avenga/couper/config/env"
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/config/runtime"
+	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
 	"github.com/avenga/couper/handler/transport"
 	"github.com/avenga/couper/logging"
 )
 
+type muxers map[string]*Mux
+
 // HTTPServer represents a configured HTTP server.
 type HTTPServer struct {
 	accessLog  *logging.AccessLog
@@ -29,7 +32,7 @@ type HTTPServer struct {
 	evalCtx    *eval.Context
 	listener   net.Listener
 	log        logrus.FieldLogger
-	mux        *Mux
+	muxers     muxers
 	port       string
 	settings   *config.Settings
 	shutdownCh chan struct{}
@@ -44,8 +47,8 @@ func NewServerList(
 	timings *runtime.HTTPTimings, srvConf runtime.ServerConfiguration) ([]*HTTPServer, func()) {
 	var list []*HTTPServer
 
-	for port, srvMux := range srvConf {
-		list = append(list, New(cmdCtx, evalCtx, log, settings, timings, port, srvMux))
+	for port, hosts := range srvConf {
+		list = append(list, New(cmdCtx, evalCtx, log, settings, timings, port, hosts))
 	}
 
 	handleShutdownFn := func() {
@@ -59,7 +62,7 @@ func NewServerList(
 // New creates a configured HTTP server.
 func New(
 	cmdCtx context.Context, evalCtx *eval.Context, log logrus.FieldLogger, settings *config.Settings,
-	timings *runtime.HTTPTimings, p runtime.Port, muxOpts *runtime.MuxOptions) *HTTPServer {
+	timings *runtime.HTTPTimings, p runtime.Port, hosts runtime.Hosts) *HTTPServer {
 	var uidFn func() string
 	if settings.RequestIDFormat == "uuid4" {
 		uidFn = func() string {
@@ -77,15 +80,20 @@ func New(
 
 	shutdownCh := make(chan struct{})
 
-	mux := NewMux(muxOpts)
-	mux.MustAddRoute(http.MethodGet, settings.HealthPath, handler.NewHealthCheck(settings.HealthPath, shutdownCh))
+	muxersList := make(muxers)
+	for host, muxOpts := range hosts {
+		mux := NewMux(muxOpts)
+		mux.MustAddRoute(http.MethodGet, settings.HealthPath, handler.NewHealthCheck(settings.HealthPath, shutdownCh))
+
+		muxersList[host] = mux
+	}
 
 	httpSrv := &HTTPServer{
 		evalCtx:    evalCtx,
 		accessLog:  logging.NewAccessLog(&logConf, log),
 		commandCtx: cmdCtx,
 		log:        log,
-		mux:        mux,
+		muxers:     muxersList,
 		port:       p.String(),
 		settings:   settings,
 		shutdownCh: shutdownCh,
@@ -169,7 +177,20 @@ func (s *HTTPServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 	req.Host = s.getHost(req)
 
-	h := s.mux.FindHandler(req)
+	host, _, err := runtime.GetHostPort(req.Host)
+	if err != nil {
+		errors.DefaultHTML.ServeError(errors.InvalidRequest).ServeHTTP(rw, req)
+	}
+
+	mux, ok := s.muxers[host]
+	if !ok {
+		mux, ok = s.muxers["*"]
+		if !ok {
+			errors.DefaultHTML.ServeError(errors.Configuration).ServeHTTP(rw, req)
+		}
+	}
+
+	h := mux.FindHandler(req)
 	w := NewRWWrapper(rw,
 		transport.ReClientSupportsGZ.MatchString(
 			req.Header.Get(transport.AcceptEncodingHeader),
@@ -179,7 +200,7 @@ func (s *HTTPServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	rw = w
 
 	if err := s.setGetBody(h, req); err != nil {
-		s.mux.opts.ErrorTpl.ServeError(err).ServeHTTP(rw, req)
+		mux.opts.ServerOptions.ServerErrTpl.ServeError(err).ServeHTTP(rw, req)
 		return
 	}
 
diff --git a/server/mux.go b/server/mux.go
index 58eda799..d8496644 100644
--- a/server/mux.go
+++ b/server/mux.go
@@ -49,7 +49,7 @@ const serverOptionsKey = "serverContextOptions"
 func NewMux(options *runtime.MuxOptions) *Mux {
 	opts := options
 	if opts == nil {
-		opts = runtime.NewMuxOptions(errors.DefaultHTML, nil)
+		opts = runtime.NewMuxOptions()
 	}
 
 	mux := &Mux{
@@ -127,46 +127,36 @@ func (m *Mux) mustAddRoute(root *pathpattern.Node, methods []string, path string
 			}},
 		}
 	}
+
 	return m
 }
 
 func (m *Mux) FindHandler(req *http.Request) http.Handler {
 	var route *openapi3filter.Route
 
-	node, srvCtxOpts, paramValues := m.match(m.endpointRoot, req)
+	node, paramValues := m.match(m.endpointRoot, req)
 	if node == nil {
 		// 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 tpl := getAPIErrorTemplate(srvCtxOpts, req.URL.Path); tpl != nil {
+		if tpl := m.getAPIErrorTemplate(req.URL.Path); tpl != nil {
 			return tpl.ServeError(errors.APIRouteNotFound)
 		}
 
-		fileHandler, fileSrvCtxOpts, exist := m.hasFileResponse(req)
+		fileHandler, exist := m.hasFileResponse(req)
 		if exist {
 			return fileHandler
 		}
-		if fileSrvCtxOpts != nil && srvCtxOpts == nil {
-			srvCtxOpts = fileSrvCtxOpts
-		}
 
-		var spaSrvCtxOpts *server.Options
-		node, spaSrvCtxOpts, paramValues = m.match(m.spaRoot, req)
-		if spaSrvCtxOpts != nil && srvCtxOpts == nil {
-			srvCtxOpts = spaSrvCtxOpts
-		}
+		node, paramValues = m.match(m.spaRoot, req)
 
 		if node == nil {
-			// no spa path?
-			if fileSrvCtxOpts != nil && isConfigured(fileSrvCtxOpts.FileBasePath) && isFileError(srvCtxOpts, req.URL.Path) {
-				return fileSrvCtxOpts.FileErrTpl.ServeError(errors.FilesRouteNotFound)
+			if isConfigured(m.opts.ServerOptions.FilesBasePath) && matchesPath(m.opts.ServerOptions.FilesBasePath, req.URL.Path) {
+				return m.opts.ServerOptions.FilesErrTpl.ServeError(errors.FilesRouteNotFound)
 			}
 
-			if srvCtxOpts != nil {
-				return srvCtxOpts.ServerErrTpl.ServeError(errors.Configuration)
-			}
 			// Fallback
-			return m.opts.ErrorTpl.ServeError(errors.Configuration)
+			return m.opts.ServerOptions.ServerErrTpl.ServeError(errors.Configuration)
 		}
 	}
 
@@ -196,96 +186,41 @@ func (m *Mux) FindHandler(req *http.Request) http.Handler {
 	return route.Handler
 }
 
-func (m *Mux) match(root *pathpattern.Node, req *http.Request) (*pathpattern.Node, *server.Options, []string) {
-	hostPath := pathpattern.PathFromHost(req.Host, false)
-	matchHostPath := req.Method + " " + utils.JoinPath(hostPath, req.URL.Path)
-	node, paramValues := root.Match(matchHostPath)
-	if _, ok := m.opts.Hosts[req.Host]; !ok && node == nil { // no specific hosts found, lookup for general path matches
-		matchPath := req.Method + " " + req.URL.Path
-		node, paramValues = root.Match(matchPath)
-	}
-
-	var srvCtxOpts *server.Options
-
-	if node == nil { // still no match, try to obtain some server configuration from suffixList
-		hostPathIdx := strings.IndexByte(matchHostPath, '/')
-	pathLoop:
-		for _, matchPath := range []string{matchHostPath[:hostPathIdx], req.Method + " "} {
-			for _, suffix := range root.Suffixes {
-				if suffix.Pattern == matchPath {
-					if len(suffix.Node.Suffixes) > 0 {
-						// FIXME some improvement, filter health routes, nested search etc. maybe mark on route.add
-						for i := len(suffix.Node.Suffixes[0].Node.Suffixes); i > 0; i-- {
-							srvCtxOpts = unwrapServerOptions(suffix.Node.Suffixes[0].Node.Suffixes[i-1])
-							if srvCtxOpts != nil {
-								break
-							}
-						}
-					} else {
-						srvCtxOpts = unwrapServerOptions(pathpattern.Suffix{Node: suffix.Node})
-					}
-					break pathLoop
-				}
-			}
-		}
-	}
-
-	if node != nil && node.Value != nil {
-		srvCtxOpts = unwrapServerOptions(pathpattern.Suffix{Node: node})
-	}
-
-	if srvCtxOpts != nil {
-		*req = *req.WithContext(context.WithValue(req.Context(), request.ServerName, srvCtxOpts.ServerName))
-	}
+func (m *Mux) match(root *pathpattern.Node, req *http.Request) (*pathpattern.Node, []string) {
+	*req = *req.WithContext(context.WithValue(req.Context(), request.ServerName, m.opts.ServerOptions.ServerName))
 
-	return node, srvCtxOpts, paramValues
+	return root.Match(req.Method + " " + req.URL.Path)
 }
 
-func (m *Mux) hasFileResponse(req *http.Request) (http.Handler, *server.Options, bool) {
-	node, srvCtxOpts, _ := m.match(m.fileRoot, req)
+func (m *Mux) hasFileResponse(req *http.Request) (http.Handler, bool) {
+	node, _ := m.match(m.fileRoot, req)
 	if node == nil {
-		return nil, srvCtxOpts, false
+		return nil, false
 	}
 
 	route := node.Value.(*openapi3filter.Route)
 	fileHandler := route.Handler
 	if p, isProtected := fileHandler.(ac.ProtectedHandler); isProtected {
-		srvCtxOpts = p.Child().(server.Context).Options()
 		if fh, ok := p.Child().(handler.HasResponse); ok {
-			return fileHandler, srvCtxOpts, fh.HasResponse(req)
+			return fileHandler, fh.HasResponse(req)
 		}
 	}
 
 	if fh, ok := fileHandler.(handler.HasResponse); ok {
-		return fileHandler, srvCtxOpts, fh.HasResponse(req)
+		return fileHandler, fh.HasResponse(req)
 	}
 
-	return fileHandler, srvCtxOpts, false
+	return fileHandler, false
 }
 
-func unwrapServerOptions(suffix pathpattern.Suffix) *server.Options {
-	if suffix.Node.Value != nil {
-		if patternNode, ok := suffix.Node.Value.(*openapi3filter.Route); ok {
-			if patternNode.Server != nil {
-				return patternNode.Server.Variables[serverOptionsKey].Default.(*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 {
+func (m *Mux) getAPIErrorTemplate(reqPath string) *errors.Template {
+	for api, path := range m.opts.ServerOptions.APIBasePaths {
 		if !isConfigured(path) {
 			continue
 		}
 
-		if isAPIError(srvOptions, path, reqPath) {
-			return srvOptions.APIErrTpl[api]
+		if isAPIError(path, m.opts.ServerOptions.FilesBasePath, m.opts.ServerOptions.SPABasePath, reqPath) {
+			return m.opts.ServerOptions.APIErrTpls[api]
 		}
 	}
 
@@ -294,25 +229,12 @@ func getAPIErrorTemplate(srvOptions *server.Options, reqPath string) *errors.Tem
 
 // isAPIError checks the path w/ and w/o the
 // trailing slash against the request path.
-func isAPIError(srvOpts *server.Options, apiPath, reqPath string) bool {
-	if srvOpts == nil {
-		return false
-	}
-	p1 := apiPath
-	p2 := apiPath
-
-	if p1 != "/" && !strings.HasSuffix(p1, "/") {
-		p1 += "/"
-	}
-	if p2 != "/" && strings.HasSuffix(p2, "/") {
-		p2 = p2[:len(p2)-len("/")]
-	}
-
-	if strings.HasPrefix(reqPath, p1) || reqPath == p2 {
-		if isConfigured(srvOpts.FileBasePath) && apiPath == srvOpts.FileBasePath {
+func isAPIError(apiPath, filesBasePath, spaBasePath, reqPath string) bool {
+	if matchesPath(apiPath, reqPath) {
+		if isConfigured(filesBasePath) && apiPath == filesBasePath {
 			return false
 		}
-		if isConfigured(srvOpts.SPABasePath) && apiPath == srvOpts.SPABasePath {
+		if isConfigured(spaBasePath) && apiPath == spaBasePath {
 			return false
 		}
 
@@ -322,14 +244,11 @@ func isAPIError(srvOpts *server.Options, apiPath, reqPath string) bool {
 	return false
 }
 
-// isFileError checks the path w/ and w/o the
+// matchesPath checks the path w/ and w/o the
 // trailing slash against the request path.
-func isFileError(srvOpts *server.Options, reqPath string) bool {
-	if srvOpts == nil {
-		return false
-	}
-	p1 := srvOpts.FileBasePath
-	p2 := srvOpts.FileBasePath
+func matchesPath(path, reqPath string) bool {
+	p1 := path
+	p2 := path
 
 	if p1 != "/" && !strings.HasSuffix(p1, "/") {
 		p1 += "/"


Test patch:
diff --git a/config/runtime/server_internal_test.go b/config/runtime/server_internal_test.go
index 22e457d2..dce5effe 100644
--- a/config/runtime/server_internal_test.go
+++ b/config/runtime/server_internal_test.go
@@ -1,10 +1,10 @@
 package runtime
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/eval"
 )
 
 func TestServer_isUnique(t *testing.T) {
@@ -37,11 +37,57 @@ func TestServer_isUnique(t *testing.T) {
 	}
 }
 
+func TestServer_getEndpointsList(t *testing.T) {
+	srvConf := &config.Server{
+		APIs: []*config.API{
+			{
+				Endpoints: []*config.Endpoint{
+					{Pattern: "/api/1"},
+					{Pattern: "/api/2"},
+				},
+			},
+		},
+		Endpoints: []*config.Endpoint{
+			{Pattern: "/free/1"},
+			{Pattern: "/free/2"},
+		},
+	}
+
+	endpoints := newEndpointMap(srvConf)
+	if l := len(endpoints); l != 4 {
+		t.Fatalf("Expected 4 endpointes, given %d", l)
+	}
+
+	checks := map[string]HandlerKind{
+		"/api/1":  api,
+		"/api/2":  api,
+		"/free/1": endpoint,
+		"/free/2": endpoint,
+	}
+
+	for pattern, kind := range checks {
+		var exist bool
+		for endpoint, parent := range endpoints {
+			if endpoint.Pattern == pattern {
+				if kind == api && 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)
+		}
+	}
+}
+
 func TestServer_validatePortHosts(t *testing.T) {
 	type args struct {
 		conf           *config.Couper
 		configuredPort int
 	}
+
 	tests := []struct {
 		name    string
 		args    args
@@ -144,69 +190,12 @@ func TestServer_validatePortHosts(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if _, _, err := validatePortHosts(tt.args.conf, tt.args.configuredPort); (err != nil) != tt.wantErr {
+			tt.args.conf.Context = eval.NewContext(nil)
+			tt.args.conf.Settings = &config.DefaultSettings
+
+			if _, err := NewServerConfiguration(tt.args.conf, nil, nil); (err != nil) != tt.wantErr {
 				t.Errorf("validatePortHosts() error = %v, wantErr %v", err, tt.wantErr)
 			}
 		})
 	}
 }
-
-func TestServer_splitWildcardHostPort(t *testing.T) {
-	for _, port := range []string{"01234", "00", "123456"} {
-		host := fmt.Sprintf("foo.de:%s", port)
-		_, _, err := splitWildcardHostPort(host, 8080)
-		if err == nil {
-			t.Errorf("Expected en error for %q, NIL given.", host)
-		}
-	}
-
-	_, _, err := splitWildcardHostPort("1", 8080)
-	if err != nil {
-		t.Errorf("Expected NIL, given %s", err)
-	}
-}
-
-func TestServer_getEndpointsList(t *testing.T) {
-	srvConf := &config.Server{
-		APIs: []*config.API{
-			{
-				Endpoints: []*config.Endpoint{
-					{Pattern: "/api/1"},
-					{Pattern: "/api/2"},
-				},
-			},
-		},
-		Endpoints: []*config.Endpoint{
-			{Pattern: "/free/1"},
-			{Pattern: "/free/2"},
-		},
-	}
-
-	endpoints := newEndpointMap(srvConf)
-	if l := len(endpoints); l != 4 {
-		t.Fatalf("Expected 4 endpointes, given %d", l)
-	}
-
-	checks := map[string]HandlerKind{
-		"/api/1":  api,
-		"/api/2":  api,
-		"/free/1": endpoint,
-		"/free/2": endpoint,
-	}
-
-	for pattern, kind := range checks {
-		var exist bool
-		for endpoint, parent := range endpoints {
-			if endpoint.Pattern == pattern {
-				if kind == api && 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)
-		}
-	}
-}
diff --git a/handler/file_test.go b/handler/file_test.go
index c4661ccd..198cae9b 100644
--- a/handler/file_test.go
+++ b/handler/file_test.go
@@ -43,7 +43,9 @@ func TestFile_ServeHTTP(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f, err := NewFile(tt.fields.basePath, path.Join(wd, tt.fields.docRootDir), srvOpts)
+			srvOpts.FilesBasePath = tt.fields.basePath
+
+			f, err := NewFile(path.Join(wd, tt.fields.docRootDir), srvOpts)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go
index 102fe91d..4158d313 100644
--- a/server/http_endpoints_test.go
+++ b/server/http_endpoints_test.go
@@ -267,3 +267,25 @@ func TestEndpoints_OAuth2(t *testing.T) {
 		}
 	}
 }
+
+func TestEndpoints_Muxing(t *testing.T) {
+	client := newClient()
+	helper := test.New(t)
+
+	shutdown, _ := newCouper(path.Join(testdataPath, "05_couper.hcl"), helper)
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/v1", nil)
+	helper.Must(err)
+
+	res, err := client.Do(req)
+	helper.Must(err)
+
+	resBytes, err := ioutil.ReadAll(res.Body)
+	helper.Must(err)
+	res.Body.Close()
+
+	if string(resBytes) != "s1" {
+		t.Errorf("Expected body 's1', given %s", resBytes)
+	}
+}
diff --git a/server/http_test.go b/server/http_test.go
index 207941f2..5eb2a740 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -17,10 +17,8 @@ import (
 
 	logrustest "github.com/sirupsen/logrus/hooks/test"
 
-	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/configload"
 	"github.com/avenga/couper/config/runtime"
-	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/internal/test"
 	"github.com/avenga/couper/server"
 )
@@ -236,41 +234,61 @@ func TestHTTPServer_ServeHTTP_Files2(t *testing.T) {
 	helper.Must(os.Chdir(currentDir)) // defer for error cases, would be to late for normal exit
 }
 
-func TestHTTPServer_ServeHTTP_UUID_Option(t *testing.T) {
+func TestHTTPServer_UUID_Common(t *testing.T) {
 	helper := test.New(t)
+	client := newClient()
 
-	type testCase struct {
-		formatOption string
-		expRegex     *regexp.Regexp
+	confPath := "testdata/settings/02_couper.hcl"
+	shutdown, logHook := newCouper(confPath, test.New(t))
+	defer shutdown()
+
+	logHook.Reset()
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil)
+	helper.Must(err)
+
+	_, err = client.Do(req)
+	helper.Must(err)
+
+	// Wait for log
+	time.Sleep(300 * time.Millisecond)
+
+	e := logHook.LastEntry()
+	if e == nil {
+		t.Fatalf("Missing log line")
 	}
 
-	for _, testcase := range []testCase{
-		{"common", regexp.MustCompile(`^[0123456789abcdefghijklmnopqrstuv]{20}$`)},
-		{"uuid4", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
-	} {
-		t.Run(testcase.formatOption, func(subT *testing.T) {
-			log, hook := logrustest.NewNullLogger()
-			settings := config.DefaultSettings
-			settings.RequestIDFormat = testcase.formatOption
-			srv := server.New(context.Background(), eval.NewContext(nil), log, &settings, &runtime.DefaultTimings, 0, nil)
-			srv.Listen()
-			defer srv.Close()
-
-			req := httptest.NewRequest(http.MethodGet, "http://"+srv.Addr()+"/", nil)
-			req.RequestURI = ""
-
-			hook.Reset()
-
-			_, err := test.NewHTTPClient().Do(req)
-			helper.Must(err)
-			time.Sleep(time.Millisecond * 10) // log hook needs some time?
-
-			if entry := hook.LastEntry(); entry == nil {
-				t.Error("Expected a log entry")
-			} else if !testcase.expRegex.MatchString(entry.Data["uid"].(string)) {
-				t.Errorf("Expected a %q uid format, got %#v", testcase.formatOption, entry.Data["uid"])
-			}
-		})
+	regexCheck := regexp.MustCompile(`^[0-9a-v]{20}$`)
+	if !regexCheck.MatchString(e.Data["uid"].(string)) {
+		t.Errorf("Expected a common uid format, got %#v", e.Data["uid"])
+	}
+}
+
+func TestHTTPServer_UUID_uuid4(t *testing.T) {
+	helper := test.New(t)
+	client := newClient()
+
+	confPath := "testdata/settings/03_couper.hcl"
+	shutdown, logHook := newCouper(confPath, test.New(t))
+	defer shutdown()
+
+	logHook.Reset()
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil)
+	helper.Must(err)
+
+	_, err = client.Do(req)
+	helper.Must(err)
+
+	// Wait for log
+	time.Sleep(300 * time.Millisecond)
+
+	e := logHook.LastEntry()
+	if e == nil {
+		t.Fatalf("Missing log line")
+	}
+
+	regexCheck := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
+	if !regexCheck.MatchString(e.Data["uid"].(string)) {
+		t.Errorf("Expected a uuid4 uid format, got %#v", e.Data["uid"])
 	}
 }
 
diff --git a/server/mux_test.go b/server/mux_test.go
index c4ff3435..b0d9fc15 100644
--- a/server/mux_test.go
+++ b/server/mux_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/config/runtime"
+	rs "github.com/avenga/couper/config/runtime/server"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/server"
 )
@@ -19,6 +20,8 @@ func TestMux_FindHandler_PathParamContext(t *testing.T) {
 		rw.WriteHeader(http.StatusNoContent)
 	})
 
+	serverOptions, _ := rs.NewServerOptions(nil)
+
 	testOptions := &runtime.MuxOptions{
 		EndpointRoutes: map[string]http.Handler{
 			"/without":              http.NotFoundHandler(),
@@ -29,6 +32,7 @@ func TestMux_FindHandler_PathParamContext(t *testing.T) {
 			"/{section}/{project}":  noContent,
 			"/project/{project}/**": noContent,
 		},
+		ServerOptions: serverOptions,
 	}
 
 	newReq := func(path string) *http.Request {
diff --git a/server/testdata/endpoints/05_couper.hcl b/server/testdata/endpoints/05_couper.hcl
new file mode 100644
index 00000000..a2640459
--- /dev/null
+++ b/server/testdata/endpoints/05_couper.hcl
@@ -0,0 +1,19 @@
+server "api-ac-1" {
+  hosts = ["example.com:8080"]
+
+  endpoint "/v1" {
+    response {
+      body = "s1"
+    }
+  }
+}
+
+server "api-ac-2" {
+  hosts = ["v1.example.com:8080"]
+
+  endpoint "/" {
+    response {
+      body = "s2"
+    }
+  }
+}
diff --git a/server/testdata/settings/02_couper.hcl b/server/testdata/settings/02_couper.hcl
new file mode 100644
index 00000000..b9aa937b
--- /dev/null
+++ b/server/testdata/settings/02_couper.hcl
@@ -0,0 +1,9 @@
+server "log-format-common" {
+  files {
+    document_root = "./"
+  }
+}
+
+settings {
+  request_id_format = "common"
+}
diff --git a/server/testdata/settings/03_couper.hcl b/server/testdata/settings/03_couper.hcl
new file mode 100644
index 00000000..5d1a5b64
--- /dev/null
+++ b/server/testdata/settings/03_couper.hcl
@@ -0,0 +1,9 @@
+server "log-format-common" {
+  files {
+    document_root = "./"
+  }
+}
+
+settings {
+  request_id_format = "uuid4"
+}