Repository: avenga/couper
Pull request #72 (2020-12-02 14:38:35)

Fix backend override rules


Patch:
diff --git a/config/api.go b/config/api.go
index 3f624f38..69186124 100644
--- a/config/api.go
+++ b/config/api.go
@@ -2,8 +2,11 @@ package config
 
 import (
 	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
+var _ Inline = &Api{}
+
 type Api struct {
 	AccessControl        []string    `hcl:"access_control,optional"`
 	CORS                 *CORS       `hcl:"cors,block"`
@@ -12,5 +15,28 @@ type Api struct {
 	DisableAccessControl []string    `hcl:"disable_access_control,optional"`
 	Endpoint             []*Endpoint `hcl:"endpoint,block"`
 	ErrorFile            string      `hcl:"error_file,optional"`
-	InlineDefinition     hcl.Body    `hcl:",remain" json:"-"`
+	Remain               hcl.Body    `hcl:",remain" json:"-"`
+}
+
+func (a Api) Body() hcl.Body {
+	return a.Remain
+}
+
+func (a Api) Schema(inline bool) *hcl.BodySchema {
+	if !inline {
+		schema, _ := gohcl.ImpliedBodySchema(a)
+		return schema
+	}
+
+	type Inline struct {
+		Backend *Backend `hcl:"backend,block"`
+	}
+	schema, _ := gohcl.ImpliedBodySchema(&Inline{})
+
+	// The API contains a backend reference, backend block is not allowed.
+	if a.Backend != "" {
+		schema.Blocks = nil
+	}
+
+	return newBackendSchema(schema, a.Body())
 }
diff --git a/config/backend.go b/config/backend.go
index ce9ec0c9..6c61306e 100644
--- a/config/backend.go
+++ b/config/backend.go
@@ -5,15 +5,21 @@ import (
 	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
+var _ Inline = &Backend{}
+
 type Backend struct {
 	ConnectTimeout   string   `hcl:"connect_timeout,optional"`
 	Name             string   `hcl:"name,label"`
-	Options          hcl.Body `hcl:",remain"`
+	Remain           hcl.Body `hcl:",remain"`
 	RequestBodyLimit string   `hcl:"request_body_limit,optional"`
 	TTFBTimeout      string   `hcl:"ttfb_timeout,optional"`
 	Timeout          string   `hcl:"timeout,optional"`
 }
 
+func (b Backend) Body() hcl.Body {
+	return b.Remain
+}
+
 func (b Backend) Schema(inline bool) *hcl.BodySchema {
 	schema, _ := gohcl.ImpliedBodySchema(b)
 	if !inline {
@@ -46,13 +52,13 @@ func (b *Backend) Merge(other *Backend) (*Backend, []hcl.Body) {
 		result.Name = other.Name
 	}
 
-	if result.Options != nil {
-		bodies = append(bodies, result.Options)
+	if result.Remain != nil {
+		bodies = append(bodies, result.Remain)
 	}
 
-	if other.Options != nil {
-		bodies = append(bodies, other.Options)
-		result.Options = other.Options
+	if other.Remain != nil {
+		bodies = append(bodies, other.Remain)
+		result.Remain = other.Remain
 	}
 
 	if other.ConnectTimeout != "" {
@@ -73,3 +79,17 @@ func (b *Backend) Merge(other *Backend) (*Backend, []hcl.Body) {
 
 	return &result, bodies
 }
+
+func newBackendSchema(schema *hcl.BodySchema, body hcl.Body) *hcl.BodySchema {
+	for i, block := range schema.Blocks {
+		// inline backend block MAY have no label
+		if block.Type == "backend" && len(block.LabelNames) > 0 {
+			// check if a backend block could be parsed with label, otherwise its an inline one without label.
+			content, _, _ := body.PartialContent(schema)
+			if content == nil || len(content.Blocks) == 0 {
+				schema.Blocks[i].LabelNames = nil
+			}
+		}
+	}
+	return schema
+}
diff --git a/config/endpoint.go b/config/endpoint.go
index 05a038a2..3be36254 100644
--- a/config/endpoint.go
+++ b/config/endpoint.go
@@ -5,12 +5,18 @@ import (
 	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
+var _ Inline = &Endpoint{}
+
 type Endpoint struct {
 	AccessControl        []string `hcl:"access_control,optional"`
 	Backend              string   `hcl:"backend,optional"`
 	DisableAccessControl []string `hcl:"disable_access_control,optional"`
-	InlineDefinition     hcl.Body `hcl:",remain" json:"-"`
 	Pattern              string   `hcl:"path,label"`
+	Remain               hcl.Body `hcl:",remain" json:"-"`
+}
+
+func (e Endpoint) Body() hcl.Body {
+	return e.Remain
 }
 
 func (e Endpoint) Schema(inline bool) *hcl.BodySchema {
@@ -24,17 +30,11 @@ func (e Endpoint) Schema(inline bool) *hcl.BodySchema {
 		Path    string   `hcl:"path,optional"`
 	}
 	schema, _ := gohcl.ImpliedBodySchema(&Inline{})
-	for i, block := range schema.Blocks {
-		// inline backend block MAY have no label
-		if block.Type == "backend" && len(block.LabelNames) > 0 {
-			schema.Blocks[i].LabelNames = nil
-		}
-	}
 
 	// The endpoint contains a backend reference, backend block is not allowed.
 	if e.Backend != "" {
 		schema.Blocks = nil
 	}
 
-	return schema
+	return newBackendSchema(schema, e.Body())
 }
diff --git a/config/inline.go b/config/inline.go
index 3c063231..862f34d1 100644
--- a/config/inline.go
+++ b/config/inline.go
@@ -3,5 +3,6 @@ package config
 import "github.com/hashicorp/hcl/v2"
 
 type Inline interface {
+	Body() hcl.Body
 	Schema(inline bool) *hcl.BodySchema
 }
diff --git a/config/runtime/body.go b/config/runtime/body.go
new file mode 100644
index 00000000..f7d9cda7
--- /dev/null
+++ b/config/runtime/body.go
@@ -0,0 +1,29 @@
+package runtime
+
+import "github.com/hashicorp/hcl/v2"
+
+var _ hcl.Body = &Body{}
+
+type Body struct {
+	hcl.BodyContent
+}
+
+func NewBody(content *hcl.BodyContent) hcl.Body {
+	return &Body{*content}
+}
+
+func (e *Body) Content(_ *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
+	return &e.BodyContent, nil
+}
+
+func (e *Body) PartialContent(_ *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
+	return &e.BodyContent, e, nil
+}
+
+func (e *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
+	return e.Attributes, nil
+}
+
+func (e *Body) MissingItemRange() hcl.Range {
+	return e.BodyContent.MissingItemRange
+}
diff --git a/config/runtime/server.go b/config/runtime/server.go
index c6f1b94e..c02206d9 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -8,6 +8,7 @@ import (
 	"net/http/httptest"
 	"os"
 	"path"
+	"reflect"
 	"strconv"
 	"strings"
 
@@ -160,7 +161,7 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
 				}
 				endpoints[cleanPattern] = true
 
-				if err := validateInlineScheme(confCtx, endpoint.InlineDefinition, endpoint); err != nil {
+				if err := validateInlineScheme(confCtx, endpoint.Remain, endpoint); err != nil {
 					return nil, err
 				}
 
@@ -174,57 +175,25 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
 				}
 
 				// lookup for backend reference, prefer endpoint definition over api one
+				var backend http.Handler
 				if endpoint.Backend != "" {
 					if _, ok := backends[endpoint.Backend]; !ok {
 						return nil, fmt.Errorf("backend %q is not defined", endpoint.Backend)
 					}
-
 					// set server context for defined backends
 					be := backends[endpoint.Backend]
-					_, remain := be.conf.Merge(&config.Backend{Options: endpoint.InlineDefinition})
-					refBackend := newProxy(confCtx, be.conf, srvConf.API.CORS, remain, log, serverOptions)
-
-					setACHandlerFn(refBackend)
-					err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
-					if err != nil {
-						return nil, err
+					_, remain := be.conf.Merge(&config.Backend{Remain: endpoint.Remain})
+					backend = newProxy(confCtx, be.conf, srvConf.API.CORS, remain, log, serverOptions)
+				} else {
+					// otherwise try to parse an inline block and fallback for api reference or inline block
+					inlineBackend, err := newInlineBackend(confCtx, conf.Bytes, backends, srvConf.API, endpoint, log, serverOptions)
+					if err != nil { // TODO hcl.diagnostics error
+						return nil, fmt.Errorf("range: %s: %v", endpoint.Remain.MissingItemRange().String(), err)
 					}
-					continue
+					backend = inlineBackend
 				}
 
-				// otherwise try to parse an inline block and fallback for api reference or inline block
-				inlineBackend, inlineConf, err := newInlineBackend(confCtx, backends, endpoint.InlineDefinition, srvConf.API.CORS, log, serverOptions)
-				if err == errorMissingBackend {
-					if srvConf.API.Backend != "" {
-						if _, ok := backends[srvConf.API.Backend]; !ok {
-							return nil, fmt.Errorf("backend %q is not defined", srvConf.API.Backend)
-						}
-						setACHandlerFn(backends[srvConf.API.Backend].handler)
-						err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
-						if err != nil {
-							return nil, err
-						}
-						continue
-					}
-					inlineBackend, inlineConf, err = newInlineBackend(confCtx, backends, srvConf.API.InlineDefinition, srvConf.API.CORS, log, serverOptions)
-					if err != nil {
-						return nil, err
-					}
-
-					if inlineConf.Name == "" && getAttribute(confCtx, "origin", inlineConf.Options, conf.Bytes) == "" {
-						return nil, fmt.Errorf("api inline backend requires an origin attribute: %q", pattern)
-					}
-				} else if err != nil { // TODO hcl.diagnostics error
-					return nil, fmt.Errorf("range: %s: %v", endpoint.InlineDefinition.MissingItemRange().String(), err)
-				}
-
-				if e := validateOrigin(
-					getAttribute(confCtx, "origin", inlineConf.Options, conf.Bytes),
-					inlineConf.Options.MissingItemRange()); e != nil {
-					return nil, e
-				}
-
-				setACHandlerFn(inlineBackend)
+				setACHandlerFn(backend)
 				err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
 				if err != nil {
 					return nil, err
@@ -265,8 +234,8 @@ func newBackendsFromDefinitions(conf *config.Gateway, confCtx *hcl.EvalContext,
 			return nil, fmt.Errorf("backend name must be unique: %q", beConf.Name)
 		}
 
-		origin := getAttribute(confCtx, "origin", beConf.Options, conf.Bytes)
-		if e := validateOrigin(origin, beConf.Options.MissingItemRange()); e != nil {
+		origin := getAttribute(confCtx, "origin", beConf.Remain, conf.Bytes)
+		if e := validateOrigin(origin, beConf.Remain.MissingItemRange()); e != nil {
 			return nil, e
 		}
 
@@ -275,7 +244,7 @@ func newBackendsFromDefinitions(conf *config.Gateway, confCtx *hcl.EvalContext,
 		srvOpts, _ := server.NewServerOptions(&config.Server{})
 		backends[beConf.Name] = backendDefinition{
 			conf:    beConf,
-			handler: newProxy(confCtx, beConf, nil, []hcl.Body{beConf.Options}, log, srvOpts),
+			handler: newProxy(confCtx, beConf, nil, []hcl.Body{beConf.Remain}, log, srvOpts),
 		}
 	}
 	return backends, nil
@@ -405,48 +374,139 @@ func configureProtectedHandler(m ac.Map, errTpl *errors.Template, parentAC, hand
 	return h
 }
 
-func newInlineBackend(evalCtx *hcl.EvalContext, backends map[string]backendDefinition, inlineDef hcl.Body, cors *config.CORS, log *logrus.Entry, srvOpts *server.Options) (http.Handler, *config.Backend, error) {
-	content, _, diags := inlineDef.PartialContent(config.Endpoint{}.Schema(true))
-	if diags.HasErrors() {
-		return nil, nil, diags
+// newInlineBackend reads partial content of block type 'backend' which could be defined within the
+// 'api' and 'endpoint' block as inline or reference one. The result gets merged with our internal defaults
+// for timing etc. first and then with a possible reference via label, if any. Backend lookups from 'definitions'
+// must consider parents path overrides.
+func newInlineBackend(
+	evalCtx *hcl.EvalContext, confBytes []byte, backends map[string]backendDefinition,
+	parentAPI *config.Api, inlineDef config.Inline, log *logrus.Entry, srvOpts *server.Options,
+) (http.Handler, error) {
+	var parentBackend *config.Backend
+	var bodies []hcl.Body
+	if parentAPI.Backend != "" {
+		be, ok := backends[parentAPI.Backend]
+		if !ok {
+			return nil, fmt.Errorf("referenced backend does not exist: %q", parentAPI.Backend)
+		}
+		parentBackend = be.conf
+		bodies = append(bodies, be.conf.Remain)
+	} else {
+		inlineBlock, err := getBackendInlineBlock(parentAPI, evalCtx)
+		if err != nil && err != errorMissingBackend {
+			return nil, err
+		} else if err == nil {
+			parentBackend = &config.Backend{}
+			diags := gohcl.DecodeBody(inlineBlock.Body, evalCtx, parentBackend)
+			if diags.HasErrors() {
+				return nil, diags
+			}
+			bodies = append(bodies, parentBackend.Remain)
+		}
 	}
 
-	if content == nil || len(content.Blocks) == 0 {
-		return nil, nil, errorMissingBackend
+	var backendConf *config.Backend
+	inlineBlock, err := getBackendInlineBlock(inlineDef, evalCtx)
+	if err != nil {
+		if err == errorMissingBackend && parentBackend != nil {
+			backendConf = parentBackend
+
+		} else {
+			return nil, err
+		}
+	} else {
+		backendConf = &config.Backend{}
+		diags := gohcl.DecodeBody(inlineBlock.Body, evalCtx, backendConf)
+		if diags.HasErrors() {
+			return nil, diags
+		}
+		bodies = append(bodies, backendConf.Body())
 	}
 
-	var inlineBlock *hcl.Block
-	for _, block := range content.Blocks {
-		if block.Type == "backend" {
-			inlineBlock = block
+	backendConf, _ = defaultBackendConf.Merge(backendConf)
+
+	// obtain the backend reference and merge with the current override
+	if inlineBlock != nil && len(inlineBlock.Labels) > 0 {
+		backendConf.Name = inlineBlock.Labels[0]
+		if beRef, ok := backends[backendConf.Name]; ok {
+			// consider existing parents, rebuild hierarchy
+			mergedBackendConf, _ := beRef.conf.Merge(backendConf)
+			bodies = append([]hcl.Body{beRef.conf.Remain}, bodies...)
+			backendConf = mergedBackendConf
+		} else {
+			return nil, fmt.Errorf("override backend %q is not defined", backendConf.Name)
 		}
 	}
-	if inlineBlock == nil {
-		return nil, nil, errorMissingBackend
+
+	// since we reference a backend we must append the current context inline definition
+	// to handle possible overrides like 'path' for endpoints. Only if the most recent definition
+	// has no own path attribute defined, use the parents one.
+	if len(bodies) > 0 && reflect.TypeOf(inlineDef) == reflect.TypeOf(&config.Endpoint{}) {
+		// The 'path' attribute is currently the only one, this section should be refined if more attributes are required.
+		const inheritableAttr = "path"
+		recentBody := bodies[len(bodies)-1]
+		attr, _ := recentBody.JustAttributes()
+		var recentPath string
+		pathAttr, ok := attr[inheritableAttr]
+		if ok {
+			pathVal, _ := pathAttr.Expr.Value(evalCtx)
+			recentPath = seetie.ValueToString(pathVal)
+		}
+		if recentPath == "" || (parentBackend != nil && recentBody == parentBackend.Body()) {
+			// and if the endpoint has defined a path attribute
+			attr, _ = inlineDef.Body().JustAttributes()
+			pathAttr, ok = attr[inheritableAttr]
+			if ok {
+				content := &hcl.BodyContent{
+					Attributes:       hcl.Attributes{inheritableAttr: pathAttr},
+					MissingItemRange: inlineDef.Body().MissingItemRange(),
+				}
+				bodies = append(bodies, NewBody(content))
+			}
+		}
+	}
+
+	var origin string
+	for i := len(bodies); i > 0; i-- {
+		if o := getAttribute(evalCtx, "origin", bodies[i-1], confBytes); o != "" {
+			origin = o
+			break
+		}
 	}
 
-	if err := validateInlineScheme(evalCtx, inlineBlock.Body, config.Backend{}); err != nil {
-		return nil, nil, err
+	if err = validateOrigin(origin, backendConf.Remain.MissingItemRange()); err != nil {
+		return nil, err
 	}
 
-	beConf := &config.Backend{}
-	diags = gohcl.DecodeBody(inlineBlock.Body, evalCtx, beConf)
+	proxy := newProxy(evalCtx, backendConf, parentAPI.CORS, bodies, log, srvOpts)
+	return proxy, nil
+}
+
+func getBackendInlineBlock(inline config.Inline, evalCtx *hcl.EvalContext) (*hcl.Block, error) {
+	content, _, diags := inline.Body().PartialContent(inline.Schema(true))
 	if diags.HasErrors() {
-		return nil, nil, diags
+		return nil, diags
 	}
 
-	beConf, _ = defaultBackendConf.Merge(beConf)
-	if len(content.Blocks[0].Labels) > 0 {
-		beConf.Name = content.Blocks[0].Labels[0]
-		if beRef, ok := backends[beConf.Name]; ok {
-			beConf, _ = beRef.conf.Merge(beConf)
-		} else {
-			return nil, nil, fmt.Errorf("override backend %q is not defined", beConf.Name)
+	if content == nil || len(content.Blocks) == 0 {
+		return nil, errorMissingBackend
+	}
+
+	var inlineBlock *hcl.Block
+	for _, block := range content.Blocks {
+		if block.Type == "backend" {
+			if err := validateInlineScheme(evalCtx, block.Body, config.Backend{}); err != nil {
+				return nil, err
+			}
+			inlineBlock = block
+			break
 		}
 	}
 
-	proxy := newProxy(evalCtx, beConf, cors, []hcl.Body{beConf.Options}, log, srvOpts)
-	return proxy, beConf, nil
+	if inlineBlock == nil {
+		return nil, errorMissingBackend
+	}
+	return inlineBlock, nil
 }
 
 func setRoutesFromHosts(srvConf *ServerConfiguration, confPort int, hosts []string, path string, handler http.Handler, kind HandlerKind) error {
diff --git a/handler/context_options.go b/handler/context_options.go
index 1e6dfe5d..5f890891 100644
--- a/handler/context_options.go
+++ b/handler/context_options.go
@@ -18,7 +18,7 @@ func NewCtxOptions(attrName string, evalCtx *hcl.EvalContext, body hcl.Body) (Op
 	var diags hcl.Diagnostics
 	var options OptionsMap
 
-	content, d := body.Content(config.Backend{}.Schema(true))
+	content, _, d := body.PartialContent(config.Backend{}.Schema(true))
 	diags = append(diags, seetie.SetSeverityLevel(d)...)
 
 	for _, attr := range content.Attributes {


Test patch:
diff --git a/config/backend_test.go b/config/backend_test.go
index 2e977e41..77dfc1ba 100644
--- a/config/backend_test.go
+++ b/config/backend_test.go
@@ -20,28 +20,28 @@ func TestBackend_Merge(t *testing.T) {
 		{"nil", Backend{}, args{nil}, nil},
 		{"empty", Backend{}, args{&Backend{}}, &Backend{}},
 		{"left", Backend{
-			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Remain: hcl.EmptyBody(),
 		}, args{&Backend{}}, &Backend{
-			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Remain: hcl.EmptyBody(),
 		}},
 		{"right", Backend{}, args{&Backend{
-			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Remain: hcl.EmptyBody(),
 		}}, &Backend{
-			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Remain: hcl.EmptyBody(),
 		}},
 		{"override", Backend{
-			Timeout: "s", ConnectTimeout: "s", RequestBodyLimit: "2M", TTFBTimeout: "t", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", RequestBodyLimit: "2M", TTFBTimeout: "t", Remain: hcl.EmptyBody(),
 		}, args{&Backend{
-			Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Options: hcl.EmptyBody(),
+			Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Remain: hcl.EmptyBody(),
 		}}, &Backend{
-			Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Options: hcl.EmptyBody(),
+			Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Remain: hcl.EmptyBody(),
 		}},
 		{"partial override", Backend{
-			Timeout: "e", ConnectTimeout: "f", TTFBTimeout: "t", Options: hcl.EmptyBody(),
+			Timeout: "e", ConnectTimeout: "f", TTFBTimeout: "t", Remain: hcl.EmptyBody(),
 		}, args{&Backend{
 			ConnectTimeout: "d",
 		}}, &Backend{
-			Timeout: "e", ConnectTimeout: "d", TTFBTimeout: "t", Options: hcl.EmptyBody(),
+			Timeout: "e", ConnectTimeout: "d", TTFBTimeout: "t", Remain: hcl.EmptyBody(),
 		}},
 	}
 	for _, tt := range tests {
@@ -49,7 +49,7 @@ func TestBackend_Merge(t *testing.T) {
 			b := &Backend{
 				ConnectTimeout:   tt.fields.ConnectTimeout,
 				Name:             tt.fields.Name,
-				Options:          tt.fields.Options,
+				Remain:           tt.fields.Remain,
 				RequestBodyLimit: tt.fields.RequestBodyLimit,
 				Timeout:          tt.fields.Timeout,
 				TTFBTimeout:      tt.fields.TTFBTimeout,
diff --git a/internal/test/test_backend.go b/internal/test/test_backend.go
index f37acde0..50bd0ed6 100644
--- a/internal/test/test_backend.go
+++ b/internal/test/test_backend.go
@@ -22,7 +22,8 @@ func NewBackend() *Backend {
 	b.srv = httptest.NewServer(b)
 
 	// test handler
-	b.mux.HandleFunc("/anything", anything)
+	b.mux.HandleFunc("/anything", createAnythingHandler(http.StatusOK))
+	b.mux.HandleFunc("/", createAnythingHandler(http.StatusNotFound))
 
 	return b
 }
@@ -39,34 +40,39 @@ func (b *Backend) Addr() string {
 	return b.srv.URL
 }
 
-func anything(rw http.ResponseWriter, req *http.Request) {
-	type anything struct {
-		Args                               url.Values
-		Headers                            http.Header
-		Host                               string
-		Path                               string
-		Method, RemoteAddr, Url, UserAgent string
-	}
+func createAnythingHandler(status int) func(rw http.ResponseWriter, req *http.Request) {
+	return func(rw http.ResponseWriter, req *http.Request) {
+		type anything struct {
+			Args                               url.Values
+			Headers                            http.Header
+			Host                               string
+			Path                               string
+			Method, RemoteAddr, Url, UserAgent string
+			ResponseStatus                     int
+		}
 
-	_ = req.ParseForm()
+		_ = req.ParseForm()
 
-	resp := &anything{
-		Args:       req.Form,
-		Headers:    req.Header.Clone(),
-		Host:       req.Host,
-		Method:     req.Method,
-		Path:       req.URL.Path,
-		RemoteAddr: req.RemoteAddr,
-		Url:        req.URL.String(),
-		UserAgent:  req.UserAgent(),
-	}
+		resp := &anything{
+			Args:           req.Form,
+			Headers:        req.Header.Clone(),
+			Host:           req.Host,
+			Method:         req.Method,
+			Path:           req.URL.Path,
+			RemoteAddr:     req.RemoteAddr,
+			Url:            req.URL.String(),
+			UserAgent:      req.UserAgent(),
+			ResponseStatus: status,
+		}
 
-	respContent, _ := json.Marshal(resp)
+		respContent, _ := json.Marshal(resp)
 
-	rw.Header().Set("Server", "couper test-backend")
-	rw.Header().Set("Date", time.Now().Format(http.TimeFormat))
-	rw.Header().Set("Content-Length", strconv.Itoa(len(respContent)))
-	rw.Header().Set("Content-Type", "application/json")
+		rw.Header().Set("Server", "couper test-backend")
+		rw.Header().Set("Date", time.Now().Format(http.TimeFormat))
+		rw.Header().Set("Content-Length", strconv.Itoa(len(respContent)))
+		rw.Header().Set("Content-Type", "application/json")
 
-	_, _ = rw.Write(respContent)
+		rw.WriteHeader(status)
+		_, _ = rw.Write(respContent)
+	}
 }
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 60704eac..d968f98b 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -44,6 +44,10 @@ func TestMain(m *testing.M) {
 func setup() {
 	println("INTEGRATION: create test backend...")
 	testBackend = test.NewBackend()
+	err := os.Setenv("COUPER_TEST_BACKEND_ADDR", testBackend.Addr())
+	if err != nil {
+		panic(err)
+	}
 
 	wd, err := os.Getwd()
 	if err != nil {
@@ -54,6 +58,9 @@ func setup() {
 
 func teardown() {
 	println("INTEGRATION: close test backend...")
+	if err := os.Unsetenv("COUPER_TEST_BACKEND_ADDR"); err != nil {
+		panic(err)
+	}
 	testBackend.Close()
 }
 
@@ -71,7 +78,7 @@ func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) {
 			backendSchema := config.Backend{}.Schema(false)
 			inlineSchema := config.Backend{}.Schema(true)
 			backendSchema.Attributes = append(backendSchema.Attributes, inlineSchema.Attributes...)
-			content, diags := backend.Options.Content(backendSchema)
+			content, diags := backend.Remain.Content(backendSchema)
 			if diags != nil && diags.HasErrors() {
 				helper.Must(diags)
 			}
@@ -81,7 +88,7 @@ func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) {
 			}
 
 			content.Attributes["origin"].Expr = hcltest.MockExprLiteral(cty.StringVal(testBackend.Addr()))
-			backend.Options = hcltest.MockBody(content)
+			backend.Remain = hcltest.MockBody(content)
 		}
 	}
 
@@ -93,9 +100,13 @@ func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) {
 		time.Sleep(time.Second / 2)
 	}
 	//log.Out = os.Stdout
+
 	go func() {
-		helper.Must(command.NewRun(ctx).Execute([]string{file}, gatewayConf, log.WithContext(ctx)))
+		if err := command.NewRun(ctx).Execute([]string{file}, gatewayConf, log.WithContext(ctx)); err != nil {
+			panic(err)
+		}
 	}()
+
 	time.Sleep(time.Second / 2)
 
 	for _, entry := range hook.AllEntries() {
@@ -584,3 +595,67 @@ func TestHTTPServer_Endpoint_Evaluation(t *testing.T) {
 
 	cleanup(shutdown, t)
 }
+
+func TestHTTPServer_Endpoint_Evaluation_Inheritance(t *testing.T) {
+	client := newClient()
+
+	for _, confFile := range []string{"02_couper.hcl", "03_couper.hcl"} {
+		confPath := path.Join("testdata/integration/endpoint_eval", confFile)
+		shutdown, _ := newCouper(confPath, test.New(t))
+
+		type expectation struct {
+			Path           string
+			ResponseStatus int
+		}
+
+		type testCase struct {
+			reqPath string
+			exp     expectation
+		}
+
+		for _, tc := range []testCase{
+			{"/endpoint1", expectation{
+				Path:           "/anything",
+				ResponseStatus: http.StatusOK,
+			}},
+			{"/endpoint2", expectation{
+				Path:           "/anything",
+				ResponseStatus: http.StatusOK,
+			}},
+			{"/endpoint3", expectation{
+				Path:           "/unset/by/endpoint",
+				ResponseStatus: http.StatusNotFound,
+			}},
+			{"/endpoint4", expectation{
+				Path:           "/anything",
+				ResponseStatus: http.StatusOK,
+			}},
+		} {
+			t.Run(confFile+"_"+tc.reqPath, func(subT *testing.T) {
+				helper := test.New(subT)
+
+				req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.reqPath, 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("%q: %q:\nwant:\t%#v\ngot:\t%#v\npayload:\n%s", confFile, tc.reqPath, tc.exp, jsonResult, string(resBytes))
+				}
+			})
+		}
+		cleanup(shutdown, t)
+	}
+}
diff --git a/server/testdata/integration/endpoint_eval/01_couper.hcl b/server/testdata/integration/endpoint_eval/01_couper.hcl
index a181ce51..08461a91 100644
--- a/server/testdata/integration/endpoint_eval/01_couper.hcl
+++ b/server/testdata/integration/endpoint_eval/01_couper.hcl
@@ -4,9 +4,13 @@ server "api" {
   api {
     error_file = "./../api_error.json"
 
+    backend "anything" { # overrides definitions: backend "anything"
+      path = "/set/by/api/unset/by/endpoint"
+    }
+
     endpoint "/{path}/{hostname}/{origin}" {
-      path = "/unset/by/backend"
-      backend {
+      path = "/set/by/endpoint/unset/by/backend"
+      backend "anything" {
         path = "/anything"
         origin = "http://${req.path_param.origin}"
         hostname = req.path_param.hostname
@@ -21,7 +25,7 @@ server "api" {
 definitions {
   # backend origin within a definition block gets replaced with the integration test "anything" server.
   backend "anything" {
-    path = "/anything"
+    path = "/not-found-anything"
     origin = "http://anyserver/"
   }
 }
diff --git a/server/testdata/integration/endpoint_eval/02_couper.hcl b/server/testdata/integration/endpoint_eval/02_couper.hcl
new file mode 100644
index 00000000..2c89d0f3
--- /dev/null
+++ b/server/testdata/integration/endpoint_eval/02_couper.hcl
@@ -0,0 +1,36 @@
+server "api" {
+  api {
+    endpoint "/endpoint1" {
+      path = "/anything"
+      backend  = "anything"
+    }
+
+    endpoint "/endpoint2" {
+     path = "/unset/by/local-backend"
+     backend "anything" {
+       path = "/anything"
+     }
+    }
+
+    # don't override path
+    endpoint "/endpoint3" {
+      backend  = "anything"
+    }
+
+    endpoint "/endpoint4" {
+      path = "/anything"
+      backend {
+		origin = env.COUPER_TEST_BACKEND_ADDR != "" ? env.COUPER_TEST_BACKEND_ADDR : "http://127.0.0.1:1"
+      }
+    }
+
+  }
+}
+
+definitions {
+  # backend origin within a definition block gets replaced with the integration test "anything" server.
+  backend "anything" {
+    path = "/unset/by/endpoint"
+    origin = "http://anyserver/"
+  }
+}
diff --git a/server/testdata/integration/endpoint_eval/03_couper.hcl b/server/testdata/integration/endpoint_eval/03_couper.hcl
new file mode 100644
index 00000000..517aaebe
--- /dev/null
+++ b/server/testdata/integration/endpoint_eval/03_couper.hcl
@@ -0,0 +1,38 @@
+server "api" {
+  api {
+
+    backend {
+      origin = env.COUPER_TEST_BACKEND_ADDR != "" ? env.COUPER_TEST_BACKEND_ADDR : "http://127.0.0.1:1"
+      path = "/unset/by/endpoint"
+    }
+
+    endpoint "/endpoint1" {
+      path = "/anything"
+    }
+
+    endpoint "/endpoint2" {
+     path = "/unset/by/local-backend"
+     backend "anything" {
+       path = "/anything"
+     }
+    }
+
+    # don't override path
+    endpoint "/endpoint3" {
+      backend  = "anything"
+    }
+
+    endpoint "/endpoint4" {
+      path = "/anything"
+    }
+
+  }
+}
+
+definitions {
+  # backend origin within a definition block gets replaced with the integration test "anything" server.
+  backend "anything" {
+    path = "/unset/by/endpoint"
+    origin = "http://anyserver/"
+  }
+}