Repository: avenga/couper
Pull request #405 (2021-12-03 17:05:20)

sequence
Determine a possible sequence of roundtrips due to variable references.

Additionally there are some internal improvements access the backend response variables for all layers like endpoint, custom logs and error handling.

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81303157..b6ddc41d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,8 +6,10 @@ Unreleased changes are available as `avenga/couper:edge` container.
 
 * **Added**
   * [`custom_log_fields`](./docs/LOGS.md#custom-logging) attribute to be able to describe a user defined map for `custom` log field enrichment ([#388](https://github.com/avenga/couper/pull/388))
-  * The `user` as context variable from a [Basic Auth](./docs/REFERENCE.md#basic-auth-block) is now accessable via `request.context.<label>.user` for successfully authenticated requests ([#402](https://github.com/avenga/couper/pull/402))
+  * The `user` as context variable from a [Basic Auth](./docs/REFERENCE.md#basic-auth-block) is now accessible via `request.context.<label>.user` for successfully authenticated requests ([#402](https://github.com/avenga/couper/pull/402))
   * [`jwt` block](./docs/REFERENCE.md#jwt-block)/[`jwt_signing_profile` block](./docs/REFERENCE.md#jwt-signing-profile-block) with ECDSA support ([#401](https://github.com/avenga/couper/issues/401))
+  * Support for [sequences](./docs/REFERENCE.md#endpoint-sequence) of outgoing endpoint requests ([#405](https://github.com/avenga/couper/issues/405))
+  * `expected_status` attribute for `request` and `proxy` block definitions which can be caught with [error handling](./docs/ERRORS.md#endpoint-related-error_handler) ([#405](https://github.com/avenga/couper/issues/405))
 
 * **Changed**
   * Missing [scope or roles claims](./docs/REFERENCE.md#jwt-block), or scope or roles claim with unsupported values are now ignored instead of causing an error ([#380](https://github.com/avenga/couper/issues/380))
diff --git a/Makefile b/Makefile
index c51a5fb0..7b3eb14a 100644
--- a/Makefile
+++ b/Makefile
@@ -28,10 +28,11 @@ test-coverage:
 	go test -short -timeout 300s -covermode=count -coverprofile=command.coverage ./command
 	go test -short -timeout 300s -covermode=count -coverprofile=config.coverage ./config
 	go test -short -timeout 300s -covermode=count -coverprofile=docs.coverage ./docs
-	# go test -short -timeout 300s -covermode=count -coverprofile=errors.coverage ./errors
+	go test -short -timeout 300s -covermode=count -coverprofile=errors.coverage ./errors
 	go test -short -timeout 300s -covermode=count -coverprofile=eval.coverage ./eval
 	go test -short -timeout 300s -covermode=count -coverprofile=handler.coverage ./handler
-	# go test -short -timeout 300s -covermode=count -coverprofile=logging.coverage ./logging
+	go test -short -timeout 300s -covermode=count -coverprofile=producer.coverage ./handler/producer
+	go test -short -timeout 300s -covermode=count -coverprofile=logging.coverage ./logging
 	go test -short -timeout 300s -covermode=count -coverprofile=server.coverage ./server
 	go test -short -timeout 300s -covermode=count -coverprofile=main.coverage ./
 
@@ -41,9 +42,10 @@ test-coverage-show:
 	go tool cover -html=command.coverage
 	go tool cover -html=config.coverage
 	go tool cover -html=docs.coverage
-	# go tool cover -html=errors.coverage
+	go tool cover -html=errors.coverage
 	go tool cover -html=eval.coverage
 	go tool cover -html=handler.coverage
-	# go tool cover -html=logging.coverage
+	go tool cover -html=producer.coverage
+	go tool cover -html=logging.coverage
 	go tool cover -html=server.coverage
 	go tool cover -html=main.coverage
diff --git a/config/body/collect.go b/config/body/collect.go
new file mode 100644
index 00000000..0ecd2169
--- /dev/null
+++ b/config/body/collect.go
@@ -0,0 +1,102 @@
+package body
+
+import (
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+)
+
+func CollectAttributes(bodies ...hcl.Body) []*hcl.Attribute {
+	allAttributes := make([]*hcl.Attribute, 0)
+
+	for _, b := range bodies {
+		switch sb := b.(type) {
+		case *hclsyntax.Body:
+			for _, attr := range sb.Attributes {
+				allAttributes = append(allAttributes, &hcl.Attribute{
+					Name:      attr.Name,
+					Expr:      attr.Expr,
+					Range:     attr.SrcRange,
+					NameRange: attr.NameRange,
+				})
+			}
+
+			for _, block := range sb.Blocks {
+				allAttributes = append(allAttributes, CollectAttributes(block.Body)...)
+			}
+		case *Body:
+			content, _, _ := sb.PartialContent(nil)
+			for _, attr := range content.Attributes {
+				allAttributes = append(allAttributes, attr)
+			}
+			for _, block := range content.Blocks {
+				allAttributes = append(allAttributes, CollectAttributes(block.Body)...)
+			}
+		case MergedBodies:
+			// top-level attrs
+			for _, attrs := range sb.JustAllAttributes() {
+				for _, attr := range attrs {
+					allAttributes = append(allAttributes, attr)
+				}
+			}
+
+			// nested block attrs
+			for _, mb := range sb {
+				allAttributes = append(allAttributes, CollectAttributes(mb)...)
+			}
+		}
+	}
+
+	return allAttributes
+}
+
+func CollectBlockTypes(bodies ...hcl.Body) []string {
+	unique := make(map[string]struct{})
+
+	addUniqueFn := func(types ...string) {
+		for _, t := range types {
+			if _, exist := unique[t]; exist {
+				continue
+			}
+			unique[t] = struct{}{}
+		}
+	}
+
+	for _, b := range bodies {
+		switch sb := b.(type) {
+		case *hclsyntax.Body:
+
+			for _, block := range sb.Blocks {
+				nested := append(append([]string{}, block.Type), CollectBlockTypes(block.Body)...)
+				addUniqueFn(nested...)
+			}
+		case *Body:
+			content, _, _ := sb.PartialContent(nil)
+			for _, block := range content.Blocks {
+				nested := append(append([]string{}, block.Type), CollectBlockTypes(block.Body)...)
+				addUniqueFn(nested...)
+			}
+		case MergedBodies:
+			// nested block
+			for _, mb := range sb {
+				nested := append([]string{}, CollectBlockTypes(mb)...)
+				addUniqueFn(nested...)
+			}
+		}
+	}
+
+	var result []string
+	for u := range unique {
+		result = append(result, u)
+	}
+
+	return result
+}
+
+func CollectExpressions(bodies ...hcl.Body) []hcl.Expression {
+	allExpressions := make([]hcl.Expression, 0)
+	for _, attr := range CollectAttributes(bodies...) {
+		allExpressions = append(allExpressions, attr.Expr)
+	}
+
+	return allExpressions
+}
diff --git a/config/configload/merged.go b/config/body/merged.go
similarity index 86%
rename from config/configload/merged.go
rename to config/body/merged.go
index f7a96f68..28977f19 100644
--- a/config/configload/merged.go
+++ b/config/body/merged.go
@@ -1,12 +1,11 @@
 // File based on "github.com/hashicorp/hcl/v2/merged.go" except diagnostic errors for duplicates
 // since we explicitly want them and apply an override.
 
-package configload
+package body
 
 import (
 	"fmt"
 
-	hclbody "github.com/avenga/couper/config/body"
 	"github.com/hashicorp/hcl/v2"
 )
 
@@ -15,7 +14,7 @@ var reportDuplicates = false
 
 // MergeBodies is like MergeFiles except it deals directly with bodies, rather
 // than with entire files.
-func MergeBodies(bodies []hcl.Body) hcl.Body {
+func MergeBodies(bodies ...hcl.Body) hcl.Body {
 	if len(bodies) == 0 {
 		// Swap out for our singleton empty body, to reduce the number of
 		// empty slices we have hanging around.
@@ -23,13 +22,13 @@ func MergeBodies(bodies []hcl.Body) hcl.Body {
 	}
 
 	// If any of the given bodies are already merged bodies, we'll unpack
-	// to flatten to a single mergedBodies, since that's conceptually simpler.
+	// to flatten to a single MergedBodies, since that's conceptually simpler.
 	// This also, as a side-effect, eliminates any empty bodies, since
 	// empties are merged bodies with no inner bodies.
 	var newLen int
 	var flatten bool
 	for _, body := range bodies {
-		if children, merged := body.(mergedBodies); merged {
+		if children, merged := body.(MergedBodies); merged {
 			newLen += len(children)
 			flatten = true
 		} else {
@@ -37,8 +36,8 @@ func MergeBodies(bodies []hcl.Body) hcl.Body {
 		}
 	}
 
-	if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside
-		return mergedBodies(bodies)
+	if !flatten { // not just newLen == len, because we might have MergedBodies with single bodies inside
+		return MergedBodies(bodies)
 	}
 
 	if newLen == 0 {
@@ -48,16 +47,16 @@ func MergeBodies(bodies []hcl.Body) hcl.Body {
 
 	newBodies := make([]hcl.Body, 0, newLen)
 	for _, body := range bodies {
-		if children, merged := body.(mergedBodies); merged {
+		if children, merged := body.(MergedBodies); merged {
 			newBodies = append(newBodies, children...)
 		} else {
 			newBodies = append(newBodies, body)
 		}
 	}
-	return mergedBodies(newBodies)
+	return MergedBodies(newBodies)
 }
 
-var emptyBody = mergedBodies([]hcl.Body{})
+var emptyBody = MergedBodies([]hcl.Body{})
 
 // EmptyBody returns a body with no content. This body can be used as a
 // placeholder when a body is required but no body content is available.
@@ -65,7 +64,7 @@ func EmptyBody() hcl.Body {
 	return emptyBody
 }
 
-type mergedBodies []hcl.Body
+type MergedBodies []hcl.Body
 
 // Content returns the content produced by applying the given schema to all
 // of the merged bodies and merging the result.
@@ -75,18 +74,18 @@ type mergedBodies []hcl.Body
 // with which to return good diagnostics. Applications working with merged
 // bodies may wish to mark all attributes as optional and then check for
 // required attributes afterwards, to produce better diagnostics.
-func (mb mergedBodies) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
+func (mb MergedBodies) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
 	// the returned body will always be empty in this case, because mergedContent
 	// will only ever call Content on the child bodies.
 	content, _, diags := mb.mergedContent(schema, false)
 	return content, diags
 }
 
-func (mb mergedBodies) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
+func (mb MergedBodies) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
 	return mb.mergedContent(schema, true)
 }
 
-func (mb mergedBodies) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
+func (mb MergedBodies) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
 	attrs := make(map[string]*hcl.Attribute)
 	var diags hcl.Diagnostics
 
@@ -106,7 +105,7 @@ func (mb mergedBodies) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
 }
 
 // MissingItemRange returns the first non-empty range if any.
-func (mb mergedBodies) MissingItemRange() hcl.Range {
+func (mb MergedBodies) MissingItemRange() hcl.Range {
 	if len(mb) == 0 {
 		// Nothing useful to return here, so we'll return some garbage.
 		return hcl.Range{
@@ -124,7 +123,7 @@ func (mb mergedBodies) MissingItemRange() hcl.Range {
 	return mb[0].MissingItemRange()
 }
 
-func (mb mergedBodies) mergedContent(schema *hcl.BodySchema, partial bool) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
+func (mb MergedBodies) mergedContent(schema *hcl.BodySchema, partial bool) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
 	// We need to produce a new schema with none of the attributes marked as
 	// required, since _any one_ of our bodies can contribute an attribute value.
 	// We'll separately check that all required attributes are present at
@@ -199,13 +198,12 @@ func (mb mergedBodies) mergedContent(schema *hcl.BodySchema, partial bool) (*hcl
 
 				diags = append(diags, mergeAttributes(contentAttrs, thisContentAttrs)...)
 
-				content.Blocks[idx].Body = MergeBodies([]hcl.Body{
+				content.Blocks[idx].Body = MergeBodies(
 					thisContentBlock.Body, // keep child blocks
-					hclbody.New(&hcl.BodyContent{
+					New(&hcl.BodyContent{
 						Attributes:       contentAttrs,
-						MissingItemRange: content.Blocks[idx].DefRange,
-					}),
-				})
+						MissingItemRange: content.Blocks[idx].DefRange}),
+				)
 			}
 		}
 	}
@@ -237,18 +235,18 @@ func (mb mergedBodies) mergedContent(schema *hcl.BodySchema, partial bool) (*hcl
 		}
 	}
 
-	leftoverBody := MergeBodies(mergedLeftovers)
+	leftoverBody := MergeBodies(mergedLeftovers...)
 	return content, leftoverBody, diags
 }
 
 // JustAllAttributes returns a list of attributes in order. Since these bodies got added as partialContent
 // we do not have to supply a scheme or handle the underlying diagnostics.
-func (mb mergedBodies) JustAllAttributes() []hcl.Attributes {
+func (mb MergedBodies) JustAllAttributes() []hcl.Attributes {
 	return mb.JustAllAttributesWithName("")
 }
 
 // JustAllAttributesWithName behaviour is the same as JustAllAttributes with a filtered slice.
-func (mb mergedBodies) JustAllAttributesWithName(name string) []hcl.Attributes {
+func (mb MergedBodies) JustAllAttributesWithName(name string) []hcl.Attributes {
 	var result []hcl.Attributes
 
 	for _, body := range mb {
diff --git a/config/configload/backend.go b/config/configload/backend.go
index d9319b94..7728319a 100644
--- a/config/configload/backend.go
+++ b/config/configload/backend.go
@@ -1,9 +1,15 @@
 package configload
 
 import (
+	"fmt"
+
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
 	"github.com/zclconf/go-cty/cty"
+
+	"github.com/avenga/couper/config"
+	hclbody "github.com/avenga/couper/config/body"
+	"github.com/avenga/couper/internal/seetie"
 )
 
 // Backends is a sequence of Backend
@@ -61,3 +67,170 @@ func (b Backends) WithName(name string) (hcl.Body, error) {
 		Summary:  "backend reference is not defined: " + name,
 	}}
 }
+
+// mergeBackendBodies appends the left side object with newly defined attributes or overrides already defined ones.
+func mergeBackendBodies(definedBackends Backends, inline config.Inline) (hcl.Body, error) {
+	var reference hcl.Body
+	if beRef, ok := inline.(config.BackendReference); ok {
+		r, err := getBackendReference(definedBackends, beRef)
+		if err != nil {
+			return nil, err
+		}
+		reference = r
+	}
+
+	content, _, diags := inline.HCLBody().PartialContent(inline.Schema(true))
+	if diags.HasErrors() {
+		return nil, diags
+	}
+
+	if content == nil {
+		if reference != nil {
+			return reference, nil
+		}
+		return nil, fmt.Errorf("configuration error: missing backend reference or inline definition")
+	}
+
+	// Apply current attributes to the referenced body.
+	if len(content.Attributes) > 0 && reference != nil {
+		reference = hclbody.MergeBodies(reference, hclbody.New(&hcl.BodyContent{
+			Attributes:       content.Attributes,
+			MissingItemRange: content.MissingItemRange}),
+		)
+	}
+
+	var backendBlock *hcl.Block
+	if backends := content.Blocks.OfType(backend); len(backends) > 0 {
+		backendBlock = backends[0]
+	} else {
+		return reference, nil
+	}
+
+	// Case: `backend {}`, anonymous backend.
+	if len(backendBlock.Labels) == 0 {
+		return backendBlock.Body, nil
+	}
+
+	// Case: `backend "reference" {}`, referenced backend.
+	refOverride, err := definedBackends.WithName(backendBlock.Labels[0])
+	if err != nil {
+		err.(hcl.Diagnostics)[0].Subject = &backendBlock.DefRange
+
+		// Case: referenced backend is not defined in definitions.
+		return nil, err
+	}
+
+	// link backend block name (label) to attribute 'name'
+	if syntaxBody, ok := backendBlock.Body.(*hclsyntax.Body); ok {
+		if refBody, ok := refOverride.(*hclsyntax.Body); ok {
+			syntaxBody.Attributes[nameLabel] = refBody.Attributes[nameLabel]
+		}
+	}
+
+	return hclbody.MergeBodies(refOverride, backendBlock.Body), nil
+}
+
+// getBackendReference tries to fetch a backend from `definitions`
+// block by a reference name, e.g. `backend = "name"`.
+func getBackendReference(definedBackends Backends, be config.BackendReference) (hcl.Body, error) {
+	name := be.Reference()
+
+	// backend string attribute just not set
+	if name == "" {
+		return nil, nil
+	}
+
+	reference, err := definedBackends.WithName(name)
+	if err != nil {
+		return nil, err // parse err
+	}
+
+	// a name is given but we have no definition
+	if body, ok := be.(config.Inline); ok {
+		if b := body.HCLBody(); reference == nil && b != nil {
+			r := b.MissingItemRange()
+			return nil, hcl.Diagnostics{&hcl.Diagnostic{
+				Subject: &r,
+				Summary: fmt.Sprintf("backend reference '%s' is not defined", name),
+			}}
+		}
+	}
+
+	return reference, nil
+}
+
+func newBackend(definedBackends Backends, inlineConfig config.Inline) (hcl.Body, error) {
+	bend, err := mergeBackendBodies(definedBackends, inlineConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	if bend == nil {
+		// Create a default backend
+		bend = hclbody.New(&hcl.BodyContent{
+			Attributes: map[string]*hcl.Attribute{
+				"name": {
+					Name: "name",
+					Expr: &hclsyntax.LiteralValueExpr{
+						Val: cty.StringVal(defaultNameLabel),
+					},
+				},
+			},
+		})
+	}
+
+	oauth2Backend, err := newOAuthBackend(definedBackends, bend)
+	if err != nil {
+		return nil, err
+	}
+
+	if oauth2Backend != nil {
+		wrapped := hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{
+			{Type: oauth2, Body: hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{
+				{Type: backend, Body: oauth2Backend},
+			}})},
+		}})
+		bend = hclbody.MergeBodies(bend, wrapped)
+	}
+
+	return bend, nil
+}
+
+func newOAuthBackend(definedBackends Backends, parent hcl.Body) (hcl.Body, error) {
+	innerContent, err := contentByType(oauth2, parent)
+	if err != nil {
+		return nil, err
+	}
+
+	oauthBlocks := innerContent.Blocks.OfType(oauth2)
+	if len(oauthBlocks) == 0 {
+		return nil, nil
+	}
+
+	backendContent, err := contentByType(backend, oauthBlocks[0].Body)
+	if err != nil {
+		return nil, err
+	}
+
+	beConfig := &config.Backend{Remain: hclbody.New(backendContent)}
+
+	attrs, _ := oauthBlocks[0].Body.JustAttributes()
+	if attrs != nil && attrs["backend"] != nil {
+		val, _ := attrs["backend"].Expr.Value(nil)
+
+		if ref := seetie.ValueToString(val); ref != "" {
+			beConfig.Name = ref
+		}
+	}
+
+	oauthBackend, err := mergeBackendBodies(definedBackends, beConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	return newBackend(definedBackends, &config.OAuth2ReqAuth{Remain: hclbody.New(&hcl.BodyContent{
+		Blocks: []*hcl.Block{
+			{Type: backend, Body: oauthBackend},
+		},
+	})})
+}
diff --git a/config/configload/endpoint.go b/config/configload/endpoint.go
new file mode 100644
index 00000000..01058cdc
--- /dev/null
+++ b/config/configload/endpoint.go
@@ -0,0 +1,244 @@
+package configload
+
+import (
+	"fmt"
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/zclconf/go-cty/cty"
+	"net/http"
+
+	"github.com/avenga/couper/config"
+	hclbody "github.com/avenga/couper/config/body"
+	"github.com/avenga/couper/config/configload/collect"
+	"github.com/avenga/couper/errors"
+)
+
+func newCatchAllEndpoint() *config.Endpoint {
+	responseBody := hclbody.New(&hcl.BodyContent{
+		Attributes: map[string]*hcl.Attribute{
+			"status": {
+				Name: "status",
+				Expr: &hclsyntax.LiteralValueExpr{
+					Val: cty.NumberIntVal(http.StatusNotFound),
+				},
+			},
+		},
+	})
+
+	return &config.Endpoint{
+		Pattern: "/**",
+		Remain:  hclbody.New(&hcl.BodyContent{}),
+		Response: &config.Response{
+			Remain: responseBody,
+		},
+	}
+}
+
+func refineEndpoints(definedBackends Backends, endpoints config.Endpoints, check bool) error {
+	var err error
+
+	for _, endpoint := range endpoints {
+		if check && endpoint.Pattern == "" {
+			var r hcl.Range
+			if endpoint.Remain != nil {
+				r = endpoint.Remain.MissingItemRange()
+			}
+			return hcl.Diagnostics{&hcl.Diagnostic{
+				Severity: hcl.DiagError,
+				Summary:  "endpoint: missing path pattern",
+				Subject:  &r,
+			}}
+		}
+
+		endpointContent := bodyToContent(endpoint.Remain)
+
+		proxies := endpointContent.Blocks.OfType(proxy)
+		requests := endpointContent.Blocks.OfType(request)
+
+		if check && len(proxies)+len(requests) == 0 && endpoint.Response == nil {
+			return hcl.Diagnostics{&hcl.Diagnostic{
+				Severity: hcl.DiagError,
+				Summary:  "missing 'default' proxy or request block, or a response definition",
+				Subject:  &endpointContent.MissingItemRange,
+			}}
+		}
+
+		proxyRequestLabelRequired := len(proxies)+len(requests) > 1
+
+		for _, proxyBlock := range proxies {
+			proxyConfig := &config.Proxy{}
+			if diags := gohcl.DecodeBody(proxyBlock.Body, envContext, proxyConfig); diags.HasErrors() {
+				return diags
+			}
+			if len(proxyBlock.Labels) > 0 {
+				proxyConfig.Name = proxyBlock.Labels[0]
+			}
+			if proxyConfig.Name == "" {
+				proxyConfig.Name = defaultNameLabel
+			}
+
+			wsEnabled, wsBody, wsErr := getWebsocketsConfig(proxyConfig)
+			if wsErr != nil {
+				return wsErr
+			}
+
+			if wsEnabled {
+				if proxyConfig.Name != defaultNameLabel {
+					return errors.Configuration.Message("websockets attribute or block is only allowed in a 'default' proxy block")
+				}
+				if proxyRequestLabelRequired || endpoint.Response != nil {
+					return errors.Configuration.Message("websockets are allowed in the endpoint; other 'proxy', 'request' or 'response' blocks are not allowed")
+				}
+
+				if wsBody != nil {
+					proxyBlock.Body = hclbody.MergeBodies(proxyBlock.Body, wsBody)
+				}
+			}
+
+			proxyConfig.Remain = proxyBlock.Body
+
+			proxyConfig.Backend, err = newBackend(definedBackends, proxyConfig)
+			if err != nil {
+				return err
+			}
+
+			endpoint.Proxies = append(endpoint.Proxies, proxyConfig)
+		}
+
+		for _, reqBlock := range requests {
+			reqConfig := &config.Request{}
+			if diags := gohcl.DecodeBody(reqBlock.Body, envContext, reqConfig); diags.HasErrors() {
+				return diags
+			}
+
+			if len(reqBlock.Labels) > 0 {
+				reqConfig.Name = reqBlock.Labels[0]
+			}
+			if reqConfig.Name == "" {
+				reqConfig.Name = defaultNameLabel
+			}
+
+			// remap request specific names for headers and query to well known ones
+			content, leftOvers, diags := reqBlock.Body.PartialContent(reqConfig.Schema(true))
+			if diags.HasErrors() {
+				return diags
+			}
+
+			if err := verifyBodyAttributes(content); err != nil {
+				return err
+			}
+
+			renameAttribute(content, "headers", "set_request_headers")
+			renameAttribute(content, "query_params", "set_query_params")
+
+			reqConfig.Remain = hclbody.MergeBodies(leftOvers, hclbody.New(content))
+
+			reqConfig.Backend, err = newBackend(definedBackends, reqConfig)
+			if err != nil {
+				return err
+			}
+
+			endpoint.Requests = append(endpoint.Requests, reqConfig)
+		}
+
+		if endpoint.Response != nil {
+			content, _, _ := endpoint.Response.HCLBody().PartialContent(config.ResponseInlineSchema)
+			_, existsBody := content.Attributes["body"]
+			_, existsJsonBody := content.Attributes["json_body"]
+			if existsBody && existsJsonBody {
+				return hcl.Diagnostics{&hcl.Diagnostic{
+					Severity: hcl.DiagError,
+					Summary:  "response can only have one of body or json_body attributes",
+					Subject:  &content.Attributes["body"].Range,
+				}}
+			}
+		}
+
+		names := map[string]struct{}{}
+		unique := map[string]struct{}{}
+		itemRange := endpoint.Remain.MissingItemRange()
+		for _, p := range endpoint.Proxies {
+			names[p.Name] = struct{}{}
+
+			if err := validLabelName(p.Name, &itemRange); err != nil {
+				return err
+			}
+
+			if proxyRequestLabelRequired {
+				if err := uniqueLabelName(unique, p.Name, &itemRange); err != nil {
+					return err
+				}
+			}
+		}
+
+		for _, r := range endpoint.Requests {
+			names[r.Name] = struct{}{}
+
+			if err = validLabelName(r.Name, &itemRange); err != nil {
+				return err
+			}
+
+			if proxyRequestLabelRequired {
+				if err = uniqueLabelName(unique, r.Name, &itemRange); err != nil {
+					return err
+				}
+			}
+		}
+
+		if _, ok := names[defaultNameLabel]; check && !ok && endpoint.Response == nil {
+			return hcl.Diagnostics{&hcl.Diagnostic{
+				Severity: hcl.DiagError,
+				Summary:  "Missing a 'default' proxy or request definition, or a response block",
+				Subject:  &itemRange,
+			}}
+		}
+
+		addSequenceDeps(names, endpoint)
+
+		epErrorHandler := collect.ErrorHandlerSetters(endpoint)
+		if err := configureErrorHandler(epErrorHandler, definedBackends); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func getWebsocketsConfig(proxyConfig *config.Proxy) (bool, hcl.Body, error) {
+	content, _, diags := proxyConfig.Remain.PartialContent(
+		&hcl.BodySchema{Blocks: []hcl.BlockHeaderSchema{{Type: "websockets"}}},
+	)
+	if diags.HasErrors() {
+		return false, nil, diags
+	}
+
+	if proxyConfig.Websockets != nil && len(content.Blocks.OfType("websockets")) > 0 {
+		return false, nil, fmt.Errorf("either websockets attribute or block is allowed")
+	}
+
+	if proxyConfig.Websockets != nil {
+		var body hcl.Body
+
+		if *proxyConfig.Websockets {
+			block := &hcl.Block{
+				Type: "websockets",
+				Body: hclbody.EmptyBody(),
+			}
+
+			body = hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{block}})
+		}
+
+		return *proxyConfig.Websockets, body, nil
+	}
+
+	return len(content.Blocks) > 0, nil, nil
+}
+
+func renameAttribute(content *hcl.BodyContent, old, new string) {
+	if attr, ok := content.Attributes[old]; ok {
+		attr.Name = new
+		content.Attributes[new] = attr
+		delete(content.Attributes, old)
+	}
+}
diff --git a/config/configload/endpoint_sequence.go b/config/configload/endpoint_sequence.go
new file mode 100644
index 00000000..bb8ca7a0
--- /dev/null
+++ b/config/configload/endpoint_sequence.go
@@ -0,0 +1,51 @@
+package configload
+
+import (
+	"github.com/hashicorp/hcl/v2"
+
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/config/body"
+)
+
+// addSequenceDeps collects possible dependencies from variables.
+func addSequenceDeps(names map[string]struct{}, endpoint *config.Endpoint) {
+	var items []config.SequenceItem
+	for _, conf := range endpoint.Proxies {
+		items = append(items, conf)
+	}
+	for _, conf := range endpoint.Requests {
+		items = append(items, conf)
+	}
+
+	for _, seqItem := range items {
+		allExpressions := body.CollectExpressions(seqItem.HCLBody())
+		for _, expr := range allExpressions {
+			for _, traversal := range expr.Variables() {
+				if traversal.RootName() != "backend_responses" || len(traversal) < 2 {
+					continue
+				}
+
+				// do we have a ref ?
+				for _, t := range traversal[1:] {
+					tr, ok := t.(hcl.TraverseAttr)
+					if !ok {
+						continue
+					}
+
+					_, ok = names[tr.Name]
+					if !ok {
+						continue
+					}
+
+					for _, i := range items {
+						if i.GetName() != tr.Name || i == seqItem {
+							continue
+						}
+						seqItem.Add(i)
+						break
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/config/configload/load.go b/config/configload/load.go
index e130b887..518a987f 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -2,18 +2,14 @@ package configload
 
 import (
 	"fmt"
-	"net/http"
 	"os"
 	"path/filepath"
 	"regexp"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
-	"github.com/hashicorp/hcl/v2/hclsyntax"
-	"github.com/zclconf/go-cty/cty"
 
 	"github.com/avenga/couper/config"
-	hclbody "github.com/avenga/couper/config/body"
 	"github.com/avenga/couper/config/configload/collect"
 	"github.com/avenga/couper/config/parser"
 	"github.com/avenga/couper/config/reader"
@@ -34,7 +30,7 @@ const (
 	request      = "request"
 	server       = "server"
 	settings     = "settings"
-	// defaultNameLabel maps the the hcl label attr 'name'.
+	// defaultNameLabel maps the hcl label attr 'name'.
 	defaultNameLabel = "default"
 )
 
@@ -313,7 +309,7 @@ func LoadConfig(body hcl.Body, src []byte, filename string) (*config.Couper, err
 				return nil, err
 			}
 
-			apiConfig.CatchAllEndpoint = createCatchAllEndpoint()
+			apiConfig.CatchAllEndpoint = newCatchAllEndpoint()
 			serverConfig.APIs = append(serverConfig.APIs, apiConfig)
 
 			apiErrorHandler := collect.ErrorHandlerSetters(apiConfig)
@@ -337,472 +333,3 @@ func LoadConfig(body hcl.Body, src []byte, filename string) (*config.Couper, err
 
 	return couperConfig, nil
 }
-
-// mergeBackendBodies appends the left side object with newly defined attributes or overrides already defined ones.
-func mergeBackendBodies(definedBackends Backends, inline config.Inline) (hcl.Body, error) {
-	var reference hcl.Body
-	if beRef, ok := inline.(config.BackendReference); ok {
-		r, err := getBackendReference(definedBackends, beRef)
-		if err != nil {
-			return nil, err
-		}
-		reference = r
-	}
-
-	content, _, diags := inline.HCLBody().PartialContent(inline.Schema(true))
-	if diags.HasErrors() {
-		return nil, diags
-	}
-
-	if content == nil {
-		if reference != nil {
-			return reference, nil
-		}
-		return nil, fmt.Errorf("configuration error: missing backend reference or inline definition")
-	}
-
-	// Apply current attributes to the referenced body.
-	if len(content.Attributes) > 0 && reference != nil {
-		reference = MergeBodies([]hcl.Body{reference, hclbody.New(&hcl.BodyContent{
-			Attributes:       content.Attributes,
-			MissingItemRange: content.MissingItemRange,
-		})})
-	}
-
-	var backendBlock *hcl.Block
-	if backends := content.Blocks.OfType(backend); len(backends) > 0 {
-		backendBlock = backends[0]
-	} else {
-		return reference, nil
-	}
-
-	// Case: `backend {}`, anonymous backend.
-	if len(backendBlock.Labels) == 0 {
-		return backendBlock.Body, nil
-	}
-
-	// Case: `backend "reference" {}`, referenced backend.
-	refOverride, err := definedBackends.WithName(backendBlock.Labels[0])
-	if err != nil {
-		err.(hcl.Diagnostics)[0].Subject = &backendBlock.DefRange
-
-		// Case: referenced backend is not defined in definitions.
-		return nil, err
-	}
-
-	// link backend block name (label) to attribute 'name'
-	if syntaxBody, ok := backendBlock.Body.(*hclsyntax.Body); ok {
-		if refBody, ok := refOverride.(*hclsyntax.Body); ok {
-			syntaxBody.Attributes[nameLabel] = refBody.Attributes[nameLabel]
-		}
-	}
-
-	return MergeBodies([]hcl.Body{refOverride, backendBlock.Body}), nil
-}
-
-// getBackendReference tries to fetch a backend from `definitions`
-// block by a reference name, e.g. `backend = "name"`.
-func getBackendReference(definedBackends Backends, be config.BackendReference) (hcl.Body, error) {
-	name := be.Reference()
-
-	// backend string attribute just not set
-	if name == "" {
-		return nil, nil
-	}
-
-	reference, err := definedBackends.WithName(name)
-	if err != nil {
-		return nil, err // parse err
-	}
-
-	// a name is given but we have no definition
-	if body, ok := be.(config.Inline); ok {
-		if b := body.HCLBody(); reference == nil && b != nil {
-			r := b.MissingItemRange()
-			return nil, hcl.Diagnostics{&hcl.Diagnostic{
-				Subject: &r,
-				Summary: fmt.Sprintf("backend reference '%s' is not defined", name),
-			}}
-		}
-	}
-
-	return reference, nil
-}
-
-func refineEndpoints(definedBackends Backends, endpoints config.Endpoints, check bool) error {
-	var err error
-
-	for _, endpoint := range endpoints {
-		if check && endpoint.Pattern == "" {
-			var r hcl.Range
-			if endpoint.Remain != nil {
-				r = endpoint.Remain.MissingItemRange()
-			}
-			return hcl.Diagnostics{&hcl.Diagnostic{
-				Severity: hcl.DiagError,
-				Summary:  "endpoint: missing path pattern",
-				Subject:  &r,
-			}}
-		}
-
-		endpointContent := bodyToContent(endpoint.Remain)
-
-		proxies := endpointContent.Blocks.OfType(proxy)
-		requests := endpointContent.Blocks.OfType(request)
-
-		if check && len(proxies)+len(requests) == 0 && endpoint.Response == nil {
-			return hcl.Diagnostics{&hcl.Diagnostic{
-				Severity: hcl.DiagError,
-				Summary:  "missing 'default' proxy or request block, or a response definition",
-				Subject:  &endpointContent.MissingItemRange,
-			}}
-		}
-
-		proxyRequestLabelRequired := len(proxies)+len(requests) > 1
-
-		for _, proxyBlock := range proxies {
-			// TODO: refactor with request construction below // almost same ( later :-) )
-			proxyConfig := &config.Proxy{}
-			if diags := gohcl.DecodeBody(proxyBlock.Body, envContext, proxyConfig); diags.HasErrors() {
-				return diags
-			}
-			if len(proxyBlock.Labels) > 0 {
-				proxyConfig.Name = proxyBlock.Labels[0]
-			}
-			if proxyConfig.Name == "" {
-				proxyConfig.Name = defaultNameLabel
-			}
-
-			wsEnabled, wsBody, wsErr := getWebsocketsConfig(proxyConfig)
-			if wsErr != nil {
-				return wsErr
-			}
-
-			if wsEnabled {
-				if proxyConfig.Name != defaultNameLabel {
-					return errors.Configuration.Message("websockets attribute or block is only allowed in a 'default' proxy block")
-				}
-				if proxyRequestLabelRequired || endpoint.Response != nil {
-					return errors.Configuration.Message("websockets are allowed in the endpoint; other 'proxy', 'request' or 'response' blocks are not allowed")
-				}
-
-				if wsBody != nil {
-					proxyBlock.Body = MergeBodies([]hcl.Body{proxyBlock.Body, wsBody})
-				}
-			}
-
-			proxyConfig.Remain = proxyBlock.Body
-
-			proxyConfig.Backend, err = newBackend(definedBackends, proxyConfig)
-			if err != nil {
-				return err
-			}
-
-			endpoint.Proxies = append(endpoint.Proxies, proxyConfig)
-		}
-
-		for _, reqBlock := range requests {
-			reqConfig := &config.Request{}
-			if diags := gohcl.DecodeBody(reqBlock.Body, envContext, reqConfig); diags.HasErrors() {
-				return diags
-			}
-
-			if len(reqBlock.Labels) > 0 {
-				reqConfig.Name = reqBlock.Labels[0]
-			}
-			if reqConfig.Name == "" {
-				reqConfig.Name = defaultNameLabel
-			}
-
-			// remap request specific names for headers and query to well known ones
-			content, leftOvers, diags := reqBlock.Body.PartialContent(reqConfig.Schema(true))
-			if diags.HasErrors() {
-				return diags
-			}
-
-			if err := verifyBodyAttributes(content); err != nil {
-				return err
-			}
-
-			renameAttribute(content, "headers", "set_request_headers")
-			renameAttribute(content, "query_params", "set_query_params")
-
-			reqConfig.Remain = MergeBodies([]hcl.Body{leftOvers, hclbody.New(content)})
-
-			reqConfig.Backend, err = newBackend(definedBackends, reqConfig)
-			if err != nil {
-				return err
-			}
-
-			endpoint.Requests = append(endpoint.Requests, reqConfig)
-		}
-
-		if endpoint.Response != nil {
-			content, _, _ := endpoint.Response.HCLBody().PartialContent(config.ResponseInlineSchema)
-			_, existsBody := content.Attributes["body"]
-			_, existsJsonBody := content.Attributes["json_body"]
-			if existsBody && existsJsonBody {
-				return hcl.Diagnostics{&hcl.Diagnostic{
-					Severity: hcl.DiagError,
-					Summary:  "response can only have one of body or json_body attributes",
-					Subject:  &content.Attributes["body"].Range,
-				}}
-			}
-		}
-
-		names := map[string]struct{}{}
-		unique := map[string]struct{}{}
-		itemRange := endpoint.Remain.MissingItemRange()
-		for _, p := range endpoint.Proxies {
-			names[p.Name] = struct{}{}
-
-			if err := validLabelName(p.Name, &itemRange); err != nil {
-				return err
-			}
-
-			if proxyRequestLabelRequired {
-				if err := uniqueLabelName(unique, p.Name, &itemRange); err != nil {
-					return err
-				}
-			}
-		}
-
-		for _, r := range endpoint.Requests {
-			names[r.Name] = struct{}{}
-
-			if err := validLabelName(r.Name, &itemRange); err != nil {
-				return err
-			}
-
-			if proxyRequestLabelRequired {
-				if err := uniqueLabelName(unique, r.Name, &itemRange); err != nil {
-					return err
-				}
-			}
-		}
-
-		if _, ok := names[defaultNameLabel]; check && !ok && endpoint.Response == nil {
-			return hcl.Diagnostics{&hcl.Diagnostic{
-				Severity: hcl.DiagError,
-				Summary:  "Missing a 'default' proxy or request definition, or a response block",
-				Subject:  &itemRange,
-			}}
-		}
-
-		epErrorHandler := collect.ErrorHandlerSetters(endpoint)
-		if err := configureErrorHandler(epErrorHandler, definedBackends); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func getWebsocketsConfig(proxyConfig *config.Proxy) (bool, hcl.Body, error) {
-	content, _, diags := proxyConfig.Remain.PartialContent(
-		&hcl.BodySchema{Blocks: []hcl.BlockHeaderSchema{{Type: "websockets"}}},
-	)
-	if diags.HasErrors() {
-		return false, nil, diags
-	}
-
-	if proxyConfig.Websockets != nil && len(content.Blocks.OfType("websockets")) > 0 {
-		return false, nil, fmt.Errorf("either websockets attribute or block is allowed")
-	}
-
-	if proxyConfig.Websockets != nil {
-		var body hcl.Body
-
-		if *proxyConfig.Websockets {
-			block := &hcl.Block{
-				Type: "websockets",
-				Body: EmptyBody(),
-			}
-
-			body = hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{block}})
-		}
-
-		return *proxyConfig.Websockets, body, nil
-	}
-
-	return len(content.Blocks) > 0, nil, nil
-}
-
-func verifyBodyAttributes(content *hcl.BodyContent) error {
-	_, existsBody := content.Attributes["body"]
-	_, existsFormBody := content.Attributes["form_body"]
-	_, existsJsonBody := content.Attributes["json_body"]
-	if existsBody && existsFormBody || existsBody && existsJsonBody || existsFormBody && existsJsonBody {
-		rangeAttr := "body"
-		if !existsBody {
-			rangeAttr = "form_body"
-		}
-		return hcl.Diagnostics{&hcl.Diagnostic{
-			Severity: hcl.DiagError,
-			Summary:  "request can only have one of body, form_body or json_body attributes",
-			Subject:  &content.Attributes[rangeAttr].Range,
-		}}
-	}
-	return nil
-}
-
-func bodyToContent(body hcl.Body) *hcl.BodyContent {
-	content := &hcl.BodyContent{
-		MissingItemRange: body.MissingItemRange(),
-	}
-	b, ok := body.(*hclsyntax.Body)
-	if !ok {
-		return content
-	}
-
-	if len(b.Attributes) > 0 {
-		content.Attributes = make(hcl.Attributes)
-	}
-	for name, attr := range b.Attributes {
-		content.Attributes[name] = &hcl.Attribute{
-			Name:      attr.Name,
-			Expr:      attr.Expr,
-			Range:     attr.Range(),
-			NameRange: attr.NameRange,
-		}
-	}
-
-	for _, block := range b.Blocks {
-		content.Blocks = append(content.Blocks, &hcl.Block{
-			Body:        block.Body,
-			DefRange:    block.DefRange(),
-			LabelRanges: block.LabelRanges,
-			Labels:      block.Labels,
-			Type:        block.Type,
-			TypeRange:   block.TypeRange,
-		})
-	}
-
-	return content
-}
-
-func contentByType(blockType string, body hcl.Body) (*hcl.BodyContent, error) {
-	headerSchema := &hcl.BodySchema{
-		Blocks: []hcl.BlockHeaderSchema{
-			{Type: blockType},
-		}}
-	content, _, diags := body.PartialContent(headerSchema)
-	if diags.HasErrors() {
-		derr := diags.Errs()[0].(*hcl.Diagnostic)
-		if derr.Summary == "Extraneous label for "+blockType { // retry with label
-			headerSchema.Blocks[0].LabelNames = []string{nameLabel}
-			content, _, diags = body.PartialContent(headerSchema)
-			if diags.HasErrors() { // due to interface nil check, do not return empty diags
-				return nil, diags
-			}
-			return content, nil
-		}
-		return nil, diags
-	}
-	return content, nil
-}
-
-func newBackend(definedBackends Backends, inlineConfig config.Inline) (hcl.Body, error) {
-	bend, err := mergeBackendBodies(definedBackends, inlineConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	if bend == nil {
-		// Create a default backend
-		bend = hclbody.New(&hcl.BodyContent{
-			Attributes: map[string]*hcl.Attribute{
-				"name": {
-					Name: "name",
-					Expr: &hclsyntax.LiteralValueExpr{
-						Val: cty.StringVal(defaultNameLabel),
-					},
-				},
-			},
-		})
-	}
-
-	oauth2Backend, err := newOAuthBackend(definedBackends, bend)
-	if err != nil {
-		return nil, err
-	}
-
-	if oauth2Backend != nil {
-		wrapped := hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{
-			{Type: oauth2, Body: hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{
-				{Type: backend, Body: oauth2Backend},
-			}})},
-		}})
-		bend = MergeBodies([]hcl.Body{bend, wrapped})
-	}
-
-	return bend, nil
-}
-
-func createCatchAllEndpoint() *config.Endpoint {
-	responseBody := hclbody.New(&hcl.BodyContent{
-		Attributes: map[string]*hcl.Attribute{
-			"status": {
-				Name: "status",
-				Expr: &hclsyntax.LiteralValueExpr{
-					Val: cty.NumberIntVal(http.StatusNotFound),
-				},
-			},
-		},
-	})
-
-	return &config.Endpoint{
-		Pattern: "/**",
-		Remain:  hclbody.New(&hcl.BodyContent{}),
-		Response: &config.Response{
-			Remain: responseBody,
-		},
-	}
-}
-
-func newOAuthBackend(definedBackends Backends, parent hcl.Body) (hcl.Body, error) {
-	innerContent, err := contentByType(oauth2, parent)
-	if err != nil {
-		return nil, err
-	}
-
-	oauthBlocks := innerContent.Blocks.OfType(oauth2)
-	if len(oauthBlocks) == 0 {
-		return nil, nil
-	}
-
-	backendContent, err := contentByType(backend, oauthBlocks[0].Body)
-	if err != nil {
-		return nil, err
-	}
-
-	beConfig := &config.Backend{Remain: hclbody.New(backendContent)}
-
-	attrs, _ := oauthBlocks[0].Body.JustAttributes()
-	if attrs != nil && attrs["backend"] != nil {
-		val, _ := attrs["backend"].Expr.Value(nil)
-
-		if ref := seetie.ValueToString(val); ref != "" {
-			beConfig.Name = ref
-		}
-	}
-
-	oauthBackend, err := mergeBackendBodies(definedBackends, beConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	return newBackend(definedBackends, &config.OAuth2ReqAuth{Remain: hclbody.New(&hcl.BodyContent{
-		Blocks: []*hcl.Block{
-			{Type: backend, Body: oauthBackend},
-		},
-	})})
-}
-
-func renameAttribute(content *hcl.BodyContent, old, new string) {
-	if attr, ok := content.Attributes[old]; ok {
-		attr.Name = new
-		content.Attributes[new] = attr
-		delete(content.Attributes, old)
-	}
-}
diff --git a/config/configload/schema.go b/config/configload/schema.go
index 16bdee42..2b153872 100644
--- a/config/configload/schema.go
+++ b/config/configload/schema.go
@@ -299,3 +299,59 @@ func uniqueErrors(errors hcl.Diagnostics) hcl.Diagnostics {
 
 	return unique
 }
+
+func bodyToContent(body hcl.Body) *hcl.BodyContent {
+	content := &hcl.BodyContent{
+		MissingItemRange: body.MissingItemRange(),
+	}
+	b, ok := body.(*hclsyntax.Body)
+	if !ok {
+		return content
+	}
+
+	if len(b.Attributes) > 0 {
+		content.Attributes = make(hcl.Attributes)
+	}
+	for name, attr := range b.Attributes {
+		content.Attributes[name] = &hcl.Attribute{
+			Name:      attr.Name,
+			Expr:      attr.Expr,
+			Range:     attr.Range(),
+			NameRange: attr.NameRange,
+		}
+	}
+
+	for _, block := range b.Blocks {
+		content.Blocks = append(content.Blocks, &hcl.Block{
+			Body:        block.Body,
+			DefRange:    block.DefRange(),
+			LabelRanges: block.LabelRanges,
+			Labels:      block.Labels,
+			Type:        block.Type,
+			TypeRange:   block.TypeRange,
+		})
+	}
+
+	return content
+}
+
+func contentByType(blockType string, body hcl.Body) (*hcl.BodyContent, error) {
+	headerSchema := &hcl.BodySchema{
+		Blocks: []hcl.BlockHeaderSchema{
+			{Type: blockType},
+		}}
+	content, _, diags := body.PartialContent(headerSchema)
+	if diags.HasErrors() {
+		derr := diags.Errs()[0].(*hcl.Diagnostic)
+		if derr.Summary == "Extraneous label for "+blockType { // retry with label
+			headerSchema.Blocks[0].LabelNames = []string{nameLabel}
+			content, _, diags = body.PartialContent(headerSchema)
+			if diags.HasErrors() { // due to interface nil check, do not return empty diags
+				return nil, diags
+			}
+			return content, nil
+		}
+		return nil, diags
+	}
+	return content, nil
+}
diff --git a/config/configload/validate.go b/config/configload/validate.go
index 888d86c5..42aaec2b 100644
--- a/config/configload/validate.go
+++ b/config/configload/validate.go
@@ -35,3 +35,21 @@ func uniqueLabelName(unique map[string]struct{}, name string, hr *hcl.Range) err
 	unique[name] = struct{}{}
 	return nil
 }
+
+func verifyBodyAttributes(content *hcl.BodyContent) error {
+	_, existsBody := content.Attributes["body"]
+	_, existsFormBody := content.Attributes["form_body"]
+	_, existsJsonBody := content.Attributes["json_body"]
+	if existsBody && existsFormBody || existsBody && existsJsonBody || existsFormBody && existsJsonBody {
+		rangeAttr := "body"
+		if !existsBody {
+			rangeAttr = "form_body"
+		}
+		return hcl.Diagnostics{&hcl.Diagnostic{
+			Severity: hcl.DiagError,
+			Summary:  "request can only have one of body, form_body or json_body attributes",
+			Subject:  &content.Attributes[rangeAttr].Range,
+		}}
+	}
+	return nil
+}
diff --git a/config/proxy.go b/config/proxy.go
index d1945e96..09567721 100644
--- a/config/proxy.go
+++ b/config/proxy.go
@@ -10,6 +10,7 @@ import (
 var (
 	_ BackendReference = &Proxy{}
 	_ Inline           = &Proxy{}
+	_ SequenceItem     = &Proxy{}
 )
 
 // Proxy represents the <Proxy> object.
@@ -21,6 +22,7 @@ type Proxy struct {
 
 	// internally used
 	Backend hcl.Body
+	depends []SequenceItem
 }
 
 // Proxies represents a list of <Proxy> objects.
@@ -40,9 +42,10 @@ func (p Proxy) HCLBody() hcl.Body {
 func (p Proxy) Inline() interface{} {
 	type Inline struct {
 		meta.Attributes
-		Backend    *Backend    `hcl:"backend,block"`
-		URL        string      `hcl:"url,optional"`
-		Websockets *Websockets `hcl:"websockets,block"`
+		Backend        *Backend    `hcl:"backend,block"`
+		ExpectedStatus []int       `hcl:"expected_status,optional"`
+		URL            string      `hcl:"url,optional"`
+		Websockets     *Websockets `hcl:"websockets,block"`
 	}
 
 	return &Inline{}
@@ -84,3 +87,15 @@ func (p Proxy) Schema(inline bool) *hcl.BodySchema {
 
 	return meta.SchemaWithAttributes(schema)
 }
+
+func (p *Proxy) Add(item SequenceItem) {
+	p.depends = append(p.depends, item)
+}
+
+func (p *Proxy) Deps() []SequenceItem {
+	return p.depends[:]
+}
+
+func (p *Proxy) GetName() string {
+	return p.Name
+}
diff --git a/config/request.go b/config/request.go
index cde0ee3c..814059ac 100644
--- a/config/request.go
+++ b/config/request.go
@@ -9,6 +9,7 @@ import (
 var (
 	_ BackendReference = &Request{}
 	_ Inline           = &Request{}
+	_ SequenceItem     = &Request{}
 )
 
 // Request represents the <Request> object.
@@ -19,6 +20,7 @@ type Request struct {
 
 	// Internally used
 	Backend hcl.Body
+	depends []SequenceItem
 }
 
 // Requests represents a list of <Requests> objects.
@@ -37,14 +39,15 @@ func (r Request) HCLBody() hcl.Body {
 // Inline implements the <Inline> interface.
 func (r Request) Inline() interface{} {
 	type Inline struct {
-		Backend     *Backend             `hcl:"backend,block"`
-		Body        string               `hcl:"body,optional"`
-		FormBody    string               `hcl:"form_body,optional"`
-		JsonBody    string               `hcl:"json_body,optional"`
-		Headers     map[string]string    `hcl:"headers,optional"`
-		Method      string               `hcl:"method,optional"`
-		QueryParams map[string]cty.Value `hcl:"query_params,optional"`
-		URL         string               `hcl:"url,optional"`
+		Backend        *Backend             `hcl:"backend,block"`
+		Body           string               `hcl:"body,optional"`
+		ExpectedStatus []int                `hcl:"expected_status,optional"`
+		FormBody       string               `hcl:"form_body,optional"`
+		Headers        map[string]string    `hcl:"headers,optional"`
+		JsonBody       string               `hcl:"json_body,optional"`
+		Method         string               `hcl:"method,optional"`
+		QueryParams    map[string]cty.Value `hcl:"query_params,optional"`
+		URL            string               `hcl:"url,optional"`
 	}
 
 	return &Inline{}
@@ -66,3 +69,15 @@ func (r Request) Schema(inline bool) *hcl.BodySchema {
 
 	return newBackendSchema(schema, r.HCLBody())
 }
+
+func (r *Request) Add(item SequenceItem) {
+	r.depends = append(r.depends, item)
+}
+
+func (r *Request) Deps() []SequenceItem {
+	return r.depends[:]
+}
+
+func (r *Request) GetName() string {
+	return r.Name
+}
diff --git a/config/request/context_key.go b/config/request/context_key.go
index b0f33941..f1934c3d 100644
--- a/config/request/context_key.go
+++ b/config/request/context_key.go
@@ -4,16 +4,17 @@ type ContextKey uint8
 
 const (
 	ContextType ContextKey = iota
+	ContextVariablesSynced
 	APIName
 	AccessControls
 	BackendName
 	BufferOptions
 	Endpoint
+	EndpointExpectedStatus
 	EndpointKind
 	Error
 	Handler
 	LogCustomAccess
-	LogCustomEvalResult
 	LogCustomUpstream
 	LogDebugLevel
 	LogEntry
diff --git a/config/runtime/endpoint.go b/config/runtime/endpoint.go
index 7a0e832a..30138801 100644
--- a/config/runtime/endpoint.go
+++ b/config/runtime/endpoint.go
@@ -2,6 +2,9 @@ package runtime
 
 import (
 	"fmt"
+	"reflect"
+	"sort"
+	"strings"
 
 	"github.com/avenga/couper/cache"
 	"github.com/sirupsen/logrus"
@@ -81,8 +84,6 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 
 	var response *producer.Response
 	// var redirect producer.Redirect // TODO: configure redirect block
-	proxies := make(producer.Proxies, 0)
-	requests := make(producer.Requests, 0)
 
 	if endpointConf.Response != nil {
 		response = &producer.Response{
@@ -91,6 +92,8 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 		blockBodies = append(blockBodies, response.Context)
 	}
 
+	var items []config.SequenceItem
+	allProxies := make(map[string]*producer.Proxy)
 	for _, proxyConf := range endpointConf.Proxies {
 		backend, innerBody, berr := newBackend(confCtx, proxyConf.Backend, log, proxyEnv, memStore)
 		if berr != nil {
@@ -101,30 +104,42 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 			Name:      proxyConf.Name,
 			RoundTrip: proxyHandler,
 		}
-		proxies = append(proxies, p)
+
+		allProxies[proxyConf.Name] = p
+		items = append(items, proxyConf)
 		blockBodies = append(blockBodies, proxyConf.Backend, innerBody, proxyConf.HCLBody())
 	}
 
+	allRequests := make(map[string]*producer.Request)
 	for _, requestConf := range endpointConf.Requests {
 		backend, innerBody, berr := newBackend(confCtx, requestConf.Backend, log, proxyEnv, memStore)
 		if berr != nil {
 			return nil, berr
 		}
 
-		requests = append(requests, &producer.Request{
+		pr := &producer.Request{
 			Backend: backend,
 			Context: requestConf.Remain,
 			Name:    requestConf.Name,
-		})
+		}
+
+		allRequests[requestConf.Name] = pr
+		items = append(items, requestConf)
 		blockBodies = append(blockBodies, requestConf.Backend, innerBody, requestConf.HCLBody())
 	}
 
+	errCh := make(chan error, 1)
+	sequences, requests, proxies := newSequences(allProxies, allRequests, errCh, items...)
+	if err := <-errCh; err != nil {
+		return nil, err
+	}
+
 	backendConf := *DefaultBackendConf
 	if diags := gohcl.DecodeBody(endpointConf.Remain, confCtx, &backendConf); diags.HasErrors() {
 		return nil, diags
 	}
 	// TODO: redirect
-	if endpointConf.Response == nil && len(proxies)+len(requests) == 0 { // && redirect == nil
+	if endpointConf.Response == nil && len(proxies)+len(requests)+len(sequences) == 0 { // && redirect == nil
 		r := endpointConf.Remain.MissingItemRange()
 		m := fmt.Sprintf("configuration error: endpoint %q requires at least one proxy, request, response or redirect block", endpointConf.Pattern)
 		return nil, hcl.Diagnostics{&hcl.Diagnostic{
@@ -145,9 +160,6 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 	}
 
 	bufferOpts := eval.MustBuffer(append(blockBodies, endpointConf.Remain)...)
-	if len(proxies)+len(requests) > 1 { // also buffer with more possible results
-		bufferOpts |= eval.BufferResponse
-	}
 
 	apiName := ""
 	if apiConf != nil {
@@ -155,15 +167,151 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 	}
 
 	return &handler.EndpointOptions{
-		APIName:      apiName,
-		Context:      endpointConf.Remain,
-		Error:        errTpl,
-		LogPattern:   endpointConf.Pattern,
-		Proxies:      proxies,
-		ReqBodyLimit: bodyLimit,
-		BufferOpts:   bufferOpts,
-		Requests:     requests,
-		Response:     response,
-		ServerOpts:   serverOptions,
+		APIName:       apiName,
+		Context:       endpointConf.Remain,
+		ErrorTemplate: errTpl,
+		LogPattern:    endpointConf.Pattern,
+		Proxies:       proxies,
+		ReqBodyLimit:  bodyLimit,
+		BufferOpts:    bufferOpts,
+		Requests:      requests,
+		Sequences:     sequences,
+		Response:      response,
+		ServerOpts:    serverOptions,
 	}, nil
 }
+
+// newSequences lookups any request related dependency and sort them into a sequence.
+// Also return left-overs for parallel usage.
+func newSequences(proxies map[string]*producer.Proxy, requests map[string]*producer.Request, errCh chan<- error,
+	items ...config.SequenceItem) (producer.Sequences, producer.Requests, producer.Proxies) {
+
+	defer func() {
+		if rc := recover(); rc != nil {
+			errCh <- rc.(error)
+		}
+		close(errCh)
+	}()
+
+	// start with default item
+	sort.SliceStable(items, func(i, j int) bool {
+		return items[i].GetName() == "default"
+	})
+
+	var allDeps [][]string
+	for _, item := range items {
+		deps := make([]string, 0)
+		seen := make([]string, 0)
+		resolveSequence(item, &deps, &seen)
+		allDeps = append(allDeps, deps)
+	}
+
+	// part of other chain? filter out
+	allDeps = func() (result [][]string) {
+	all:
+		for _, deps := range allDeps {
+			if len(deps) == 1 {
+				continue
+			}
+
+			for _, otherDeps := range allDeps {
+				if len(deps) >= len(otherDeps) {
+					continue
+				}
+				if reflect.DeepEqual(deps, otherDeps[:len(deps)]) {
+					continue all
+				}
+			}
+			result = append(result, deps)
+		}
+
+		return result
+	}()
+
+	var reqs producer.Requests
+	var ps producer.Proxies
+	var seqs producer.Sequences
+
+	for _, deps := range allDeps {
+		var seq producer.Sequence
+		for _, dep := range deps {
+			if p, ok := proxies[dep]; ok {
+				seq = append(seq, &producer.SequenceItem{
+					Backend: p.RoundTrip,
+					Name:    p.Name,
+				})
+			}
+			if r, ok := requests[dep]; ok {
+				seq = append(seq, &producer.SequenceItem{
+					Backend: r.Backend,
+					Context: r.Context,
+					Name:    r.Name,
+				})
+			}
+		}
+		seqs = append(seqs, seq)
+	}
+
+proxyLeftovers:
+	for name, p := range proxies {
+		for _, deps := range allDeps {
+			for _, dep := range deps {
+				if name == dep {
+					continue proxyLeftovers
+				}
+			}
+		}
+		ps = append(ps, p)
+	}
+
+reqLeftovers:
+	for name, r := range requests {
+		for _, deps := range allDeps {
+			for _, dep := range deps {
+				if name == dep {
+					continue reqLeftovers
+				}
+			}
+		}
+		reqs = append(reqs, r)
+	}
+
+	return seqs, reqs, ps
+}
+
+func resolveSequence(item config.SequenceItem, resolved, seen *[]string) {
+	name := item.GetName()
+	*seen = append(*seen, name)
+	for _, dep := range item.Deps() {
+		if !containsString(resolved, dep.GetName()) {
+			if !containsString(seen, dep.GetName()) {
+				resolveSequence(dep, resolved, seen)
+				continue
+			}
+
+			r := hcl.Range{} // try to obtain some config context
+			if b, ok := item.(interface{ HCLBody() hcl.Body }); ok {
+				r = b.HCLBody().MissingItemRange()
+			}
+			err := &hcl.Diagnostic{
+				Detail:   "circular sequence reference: " + strings.Join(*resolved, ",") + ": " + dep.GetName(),
+				Severity: hcl.DiagError,
+				Subject:  &r,
+				Summary:  "configuration error",
+			}
+			panic(err)
+
+		}
+	}
+
+	*resolved = append(*resolved, name)
+}
+
+func containsString(slice *[]string, needle string) bool {
+	for _, n := range *slice {
+		if n == needle {
+			return true
+		}
+	}
+	return false
+}
diff --git a/config/runtime/error_handler.go b/config/runtime/error_handler.go
index 81ff1ee9..21faf590 100644
--- a/config/runtime/error_handler.go
+++ b/config/runtime/error_handler.go
@@ -45,13 +45,13 @@ func newErrorHandler(ctx *hcl.EvalContext, opts *protectedOptions, log *logrus.E
 				if err != nil {
 					return nil, err
 				}
-				if epOpts.Error == nil || h.ErrorFile == "" {
-					epOpts.Error = opts.epOpts.Error
+				if epOpts.ErrorTemplate == nil || h.ErrorFile == "" {
+					epOpts.ErrorTemplate = opts.epOpts.ErrorTemplate
 				}
 
-				epOpts.Error = epOpts.Error.WithContextFunc(func(rw http.ResponseWriter, r *http.Request) {
+				epOpts.ErrorTemplate = epOpts.ErrorTemplate.WithContextFunc(func(rw http.ResponseWriter, r *http.Request) {
 					beresp := &http.Response{Header: rw.Header()}
-					_ = eval.ApplyResponseContext(r.Context(), contextBody, beresp)
+					_ = eval.ApplyResponseContext(eval.ContextFromRequest(r).HCLContextSync(), contextBody, beresp)
 				})
 
 				if epOpts.Response != nil && reflect.DeepEqual(epOpts.Response.Context, emptyBody) {
@@ -59,10 +59,11 @@ func newErrorHandler(ctx *hcl.EvalContext, opts *protectedOptions, log *logrus.E
 				}
 
 				epOpts.LogHandlerKind = "error_" + k
+				epOpts.IsErrorHandler = true
 				kindsHandler[k] = handler.NewEndpoint(epOpts, log, nil)
 			}
 		}
 	}
 
-	return handler.NewErrorHandler(kindsHandler, opts.epOpts.Error), nil
+	return handler.NewErrorHandler(kindsHandler, opts.epOpts.ErrorTemplate), nil
 }
diff --git a/config/runtime/server.go b/config/runtime/server.go
index 2bca1448..7d1c8522 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"net"
 	"net/http"
-	"net/http/httptest"
 	"path"
 	"reflect"
 	"strconv"
@@ -89,48 +88,11 @@ func GetHostPort(hostPort string) (string, int, error) {
 	return host, port, nil
 }
 
-func bodiesWithACBodies(defs *config.Definitions, body hcl.Body, ac, dac []string) []hcl.Body {
-	bodies := []hcl.Body{body}
-
-	allAccessControls := collect.ErrorHandlerSetters(defs)
-
-	for _, ehs := range allAccessControls {
-		acConf, ok := ehs.(config.Body)
-		if !ok {
-			continue
-		}
-
-		t := reflect.ValueOf(acConf)
-		elem := t
-
-		if t.Kind() == reflect.Ptr {
-			elem = t.Elem()
-		}
-
-		nameValue := elem.FieldByName("Name")
-		if !nameValue.CanInterface() {
-			continue
-		}
-
-		for _, name := range config.NewAccessControl(ac, dac).List() {
-			if value, vk := nameValue.Interface().(string); vk && value == name {
-				bodies = append(bodies, acConf.HCLBody())
-			}
-		}
-	}
-
-	return bodies
-}
-
 // 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) {
-	// confCtx is created to evaluate request / response related configuration errors on start.
-	noopReq, _ := http.NewRequest(http.MethodGet, "https://couper.io", nil)
-	noopResp := httptest.NewRecorder().Result()
-	noopResp.Request = noopReq
-	evalContext := conf.Context.Value(request.ContextType).(*eval.Context)
-	confCtx := evalContext.WithClientRequest(noopReq).WithBeresps(noopResp).HCLContext()
+	evalContext := conf.Context.Value(request.ContextType).(*eval.Context) // usually environment vars
+	confCtx := evalContext.HCLContext()
 
 	oidcConfigs, ocErr := configureOidcConfigs(conf, confCtx, log, memStore)
 	if ocErr != nil {
@@ -180,10 +142,8 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 			}
 		}
 
-		serverBodies := bodiesWithACBodies(
-			conf.Definitions, srvConf.Remain,
-			srvConf.AccessControl, srvConf.DisableAccessControl,
-		)
+		serverBodies := bodiesWithACBodies(conf.Definitions, srvConf.AccessControl, srvConf.DisableAccessControl)
+		serverBodies = append(serverBodies, srvConf.Remain)
 
 		var spaHandler http.Handler
 		if srvConf.Spa != nil {
@@ -196,7 +156,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl),
 				config.NewAccessControl(srvConf.Spa.AccessControl, srvConf.Spa.DisableAccessControl),
 				&protectedOptions{
-					epOpts:       &handler.EndpointOptions{Error: serverOptions.ServerErrTpl},
+					epOpts:       &handler.EndpointOptions{ErrorTemplate: serverOptions.ServerErrTpl},
 					handler:      spaHandler,
 					memStore:     memStore,
 					proxyFromEnv: conf.Settings.NoProxyFromEnv,
@@ -213,12 +173,9 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 
 			spaHandler = middleware.NewCORSHandler(corsOptions, spaHandler)
 
-			spaBodies := bodiesWithACBodies(
-				conf.Definitions, srvConf.Spa.Remain,
-				srvConf.Spa.AccessControl, srvConf.Spa.DisableAccessControl,
-			)
+			spaBodies := bodiesWithACBodies(conf.Definitions, srvConf.Spa.AccessControl, srvConf.Spa.DisableAccessControl)
 			spaHandler = middleware.NewCustomLogsHandler(
-				append(serverBodies, spaBodies...), spaHandler, "",
+				append(serverBodies, append(spaBodies, srvConf.Spa.Remain)...), spaHandler, "",
 			)
 
 			for _, spaPath := range srvConf.Spa.Paths {
@@ -243,7 +200,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl),
 				config.NewAccessControl(srvConf.Files.AccessControl, srvConf.Files.DisableAccessControl),
 				&protectedOptions{
-					epOpts:       &handler.EndpointOptions{Error: serverOptions.FilesErrTpl},
+					epOpts:       &handler.EndpointOptions{ErrorTemplate: serverOptions.FilesErrTpl},
 					handler:      fileHandler,
 					memStore:     memStore,
 					proxyFromEnv: conf.Settings.NoProxyFromEnv,
@@ -260,12 +217,9 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 
 			fileHandler = middleware.NewCORSHandler(corsOptions, fileHandler)
 
-			fileBodies := bodiesWithACBodies(
-				conf.Definitions, srvConf.Files.Remain,
-				srvConf.Files.AccessControl, srvConf.Files.DisableAccessControl,
-			)
+			fileBodies := bodiesWithACBodies(conf.Definitions, srvConf.Files.AccessControl, srvConf.Files.DisableAccessControl)
 			fileHandler = middleware.NewCustomLogsHandler(
-				append(serverBodies, fileBodies...), fileHandler, "",
+				append(serverBodies, append(fileBodies, srvConf.Files.Remain)...), fileHandler, "",
 			)
 
 			err = setRoutesFromHosts(serverConfiguration, portsHosts, serverOptions.FilesBasePath, fileHandler, files)
@@ -305,7 +259,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				return nil, err
 			}
 
-			scopeDefinitions := ACDefinitions{ // misuse of definitions obj for now
+			errorHandlerDefinitions := ACDefinitions{ // misuse of definitions obj for now
 				"endpoint": &AccessControl{ErrorHandler: endpointConf.ErrorHandler},
 			}
 
@@ -317,14 +271,26 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 
 				modifier = []hcl.Body{parentAPI.Remain, srvConf.Remain}
 
-				scopeDefinitions["api"] = &AccessControl{ErrorHandler: parentAPI.ErrorHandler}
+				errorHandlerDefinitions["api"] = &AccessControl{ErrorHandler: parentAPI.ErrorHandler}
 			}
 			epOpts.LogHandlerKind = kind.String()
 
 			var epHandler http.Handler
 			if parentAPI != nil && parentAPI.CatchAllEndpoint == endpointConf {
-				epHandler = epOpts.Error.ServeError(errors.RouteNotFound)
+				epHandler = epOpts.ErrorTemplate.WithError(errors.RouteNotFound)
 			} else {
+				epErrorHandler, err := newErrorHandler(confCtx, &protectedOptions{
+					epOpts:       epOpts,
+					memStore:     memStore,
+					proxyFromEnv: conf.Settings.NoProxyFromEnv,
+					srvOpts:      serverOptions,
+				}, log, errorHandlerDefinitions, "api", "endpoint")
+				if err != nil {
+					return nil, err
+				}
+				if epErrorHandler != nil {
+					epOpts.ErrorHandler = epErrorHandler
+				}
 				epHandler = handler.NewEndpoint(epOpts, log, modifier)
 			}
 
@@ -339,7 +305,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				memStore:     memStore,
 				proxyFromEnv: conf.Settings.NoProxyFromEnv,
 				srvOpts:      serverOptions,
-			}, log, scopeDefinitions, "api", "endpoint")
+			}, log, errorHandlerDefinitions, "api", "endpoint")
 			if err != nil {
 				return nil, err
 			}
@@ -349,7 +315,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 
 			accessControl := newAC(srvConf, parentAPI)
 			if parentAPI != nil && parentAPI.CatchAllEndpoint == endpointConf {
-				protectedHandler = epOpts.Error.ServeError(errors.RouteNotFound)
+				protectedHandler = epOpts.ErrorTemplate.WithError(errors.RouteNotFound)
 			}
 
 			epHandler, err = configureProtectedHandler(accessControls, confCtx, accessControl,
@@ -374,18 +340,12 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 
 			bodies := serverBodies
 			if parentAPI != nil {
-				apiBodies := bodiesWithACBodies(
-					conf.Definitions, parentAPI.Remain,
-					parentAPI.AccessControl, parentAPI.DisableAccessControl,
-				)
-				bodies = append(bodies, apiBodies...)
-			}
-			epBodies := bodiesWithACBodies(
-				conf.Definitions, endpointConf.Remain,
-				endpointConf.AccessControl, endpointConf.DisableAccessControl,
-			)
+				apiBodies := bodiesWithACBodies(conf.Definitions, parentAPI.AccessControl, parentAPI.DisableAccessControl)
+				bodies = append(bodies, append(apiBodies, parentAPI.Remain)...)
+			}
+			bodies = append(bodies, bodiesWithACBodies(conf.Definitions, endpointConf.AccessControl, endpointConf.DisableAccessControl)...)
 			epHandler = middleware.NewCustomLogsHandler(
-				append(bodies, epBodies...), epHandler, epOpts.LogHandlerKind,
+				append(bodies, endpointConf.Remain), epHandler, epOpts.LogHandlerKind,
 			)
 
 			endpointHandlers[endpointConf] = epHandler
@@ -399,6 +359,39 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 	return serverConfiguration, nil
 }
 
+func bodiesWithACBodies(defs *config.Definitions, ac, dac []string) []hcl.Body {
+	var bodies []hcl.Body
+
+	allAccessControls := collect.ErrorHandlerSetters(defs)
+
+	for _, ehs := range allAccessControls {
+		acConf, ok := ehs.(config.Body)
+		if !ok {
+			continue
+		}
+
+		t := reflect.ValueOf(acConf)
+		elem := t
+
+		if t.Kind() == reflect.Ptr {
+			elem = t.Elem()
+		}
+
+		nameValue := elem.FieldByName("Name")
+		if !nameValue.CanInterface() {
+			continue
+		}
+
+		for _, name := range config.NewAccessControl(ac, dac).List() {
+			if value, vk := nameValue.Interface().(string); vk && value == name {
+				bodies = append(bodies, acConf.HCLBody())
+			}
+		}
+	}
+
+	return bodies
+}
+
 func newScopeMaps(parentAPI *config.API, endpoint *config.Endpoint) ([]map[string]string, error) {
 	var scopeMaps []map[string]string
 	if parentAPI != nil {
diff --git a/config/sequence.go b/config/sequence.go
new file mode 100644
index 00000000..264dadc6
--- /dev/null
+++ b/config/sequence.go
@@ -0,0 +1,8 @@
+package config
+
+type SequenceItem interface {
+	Body
+	Add(item SequenceItem)
+	Deps() []SequenceItem
+	GetName() string
+}
diff --git a/docs/ERRORS.md b/docs/ERRORS.md
index de87e4dd..10712178 100644
--- a/docs/ERRORS.md
+++ b/docs/ERRORS.md
@@ -5,8 +5,12 @@
   - [Error messages](#error-messages)
   - [Access control error_handler](#access-control-error_handler)
   - [Scope related error_handler](#scope-related-error_handler)
-    - [error_handler specification](#error_handler-specification)
-    - [Error types](#error-types)
+  - [Endpoint related error_handler](#endpoint-related-error_handler)
+  - [error_handler specification](#error_handler-specification)
+  - [Error types](#error-types)
+    - [Access control error types](#access-control-error-types)
+    - [API/endpoint error types](#api-and-endpoint-error-types)
+    - [Endpoint error types](#endpoint-error-types)
 
 ## Introduction
 
@@ -29,7 +33,12 @@ Since an [AC](#access-control-error_handler) error handler is rather general tha
 same access-control. Reacting to those more path specific errors you can define one or more `error_handler` blocks within
 an `api` or `endpoint` block.
 
-### `error_handler` specification
+## Endpoint related `error_handler`
+
+A [sequence](REFERENCE.md#endpoint-sequence), a simple `request` or `proxy` error can be handled in combination with the `expected_status` attribute for `request`
+and `proxy` block definitions and an `error_handler` definition with the [related](#endpoint-error-types) type label.
+
+## `error_handler` specification
 
 The error handler label specifies which [error type](#error-types)
 should be handled. Multiple labels are allowed. The label can be omitted to catch all errors which are related to this access control definition. This has the same behavior as the error type `*`, that catches all errors explicitly.
@@ -49,12 +58,14 @@ error_handler "jwt_token_missing" {
 }
 ```
 
-### Error types
+## Error types
 
 All errors have a specific type. You can find it in the log field `error_type`. Furthermore, errors can be associated with a list of less specific types. Your error handlers will be evaluated from the most to the least specific one. Only the first matching error handler is executed.
 
+### Access control error types
+
 | Type (and super types)                          | Description                                                                                      | Default handling                                                            |
-| :---------------------------------------------- | :----------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- |
+|:------------------------------------------------|:-------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------|
 | `basic_auth`                                    | All `basic_auth` related errors, e.g. unknown user or wrong password.                            | Send error template with status `401` and `WWW-Authenticate: Basic` header. |
 | `basic_auth_credentials_missing` (`basic_auth`) | Client does not provide any credentials.                                                         | Send error template with status `401` and `WWW-Authenticate: Basic` header. |
 | `jwt`                                           | All `jwt` related errors.                                                                        | Send error template with status `403`.                                      |
@@ -63,6 +74,18 @@ All errors have a specific type. You can find it in the log field `error_type`.
 | `jwt_token_invalid` (`jwt`)                     | The token is not sufficient, e.g. because required claims are missing or have unexpected values. | Send error template with status `403`.                                      |
 | `saml2`                                         | All `saml2` related errors                                                                       | Send error template with status `403`.                                      |
 | `oauth2`                                        | All `beta_oauth2`/`oidc` related errors                                                          | Send error template with status `403`.                                      |
+
+### API and endpoint error types
+
+| Type (and super types)                          | Description                                                                                      | Default handling                                                            |
+|:------------------------------------------------|:-------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------|
 | `beta_scope`                                    | All `beta_scope` related errors                                                                  | Send error template with status `403`.                                      |
 | `beta_insufficient_scope`                       | The request is not in the scope granted to the requester.                                        | Send error template with status `403`.                                      |
 | `beta_operation_denied`                         | The request method is not permitted.                                                             | Send error template with status `403`.                                      |
+
+### Endpoint error types
+
+| Type (and super types)                          | Description                                                                                      | Default handling                                                            |
+|:------------------------------------------------|:-------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------|
+| `sequence`                                      | A `request` or `proxy` block request has been failed while depending on another one              | Send error template with status `502`.                                      |
+| `unexpected_status`                             | A `request` or `proxy` block response status code does not match the to `expected_status` list   | Send error template with status `502`.                                      |
diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index 8ef2d439..9e2ada7b 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -7,6 +7,7 @@
     - [SPA Block](#spa-block)
     - [API Block](#api-block)
     - [Endpoint Block](#endpoint-block)
+      - [Endpoint Sequence](#endpoint-sequence)
     - [Proxy Block](#proxy-block)
     - [Request Block](#request-block)
     - [Response Block](#response-block)
@@ -120,20 +121,25 @@ changes the path for the outgoing request (compare
 [path mapping example](./README.md#routing-path-mapping)). Each `endpoint` block must
 produce an explicit or implicit client response.
 
-|Block name|Context|Label|Nested block(s)|
-| :-----------| :-----------| :-----------| :-----------|
-|`endpoint`| [Server Block](#server-block), [API Block](#api-block) |&#9888; required, defines the path suffix for incoming client requests | [Proxy Block(s)](#proxy-block),  [Request Block(s)](#request-block), [Response Block](#response-block), [Error Handler Block](ERRORS.md#error_handler-specification) |
+| Block name | Context                                                | Label                                                                  | Nested block(s)                                                                                                                                                      |
+|:-----------|:-------------------------------------------------------|:-----------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `endpoint` | [Server Block](#server-block), [API Block](#api-block) | &#9888; required, defines the path suffix for incoming client requests | [Proxy Block(s)](#proxy-block),  [Request Block(s)](#request-block), [Response Block](#response-block), [Error Handler Block](ERRORS.md#error_handler-specification) |
 
 <!-- TODO: decide how to place "modifier" in the reference table - same for other block which allow modifiers -->
 
-| Attribute(s) | Type |Default|Description|Characteristic(s)| Example|
-| :------------------------------ | :--------------- | :--------------- | :--------------- | :--------------- | :--------------- |
-|`request_body_limit`  |string|`64MiB`|Configures the maximum buffer size while accessing `request.form_body` or `request.json_body` content.|&#9888; Valid units are: `KiB, MiB, GiB`|`request_body_limit = "200KiB"`|
-| `path`|string|-|Changeable part of the upstream URL. Changes the path suffix of the outgoing request.|-|-|
-|`access_control`   |list|-|Sets predefined [Access Control](#access-control) for `endpoint` block context.|-| `access_control = ["foo"]`|
-| `beta_scope` |string or object|-|Scope value required to use this endpoint (see [error type](../ERRORS.md#error-types) `beta_insufficient_scope`).|If the value is a string, the same scope value applies to all request methods. If there are different scope values for different request methods, use an object with the request methods as keys and string values. Methods not specified in this object are not permitted (see [error type](../ERRORS.md#error-types) `beta_operation_denied`). `"*"` is the key for "all other methods". A value `""` means "no (additional) scope required".| `beta_scope = "read"` or `beta_scope = { post = "write", "*" = "" }`|
-| `custom_log_fields` | map | - | Defines log fields for [Custom Logging](LOGS.md#custom-logging). | &#9888; Inherited by nested blocks. | - |
-|[Modifiers](#modifiers) |-|-|-|-|-|
+| Attribute(s)            | Type             | Default | Description                                                                                                       | Characteristic(s)                                                                                                                                                                                                                                                                                                                                                                                                                               | Example                                                              |
+|:------------------------|:-----------------|:--------|:------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|
+| `request_body_limit`    | string           | `64MiB` | Configures the maximum buffer size while accessing `request.form_body` or `request.json_body` content.            | &#9888; Valid units are: `KiB, MiB, GiB`                                                                                                                                                                                                                                                                                                                                                                                                        | `request_body_limit = "200KiB"`                                      |
+| `path`                  | string           | -       | Changeable part of the upstream URL. Changes the path suffix of the outgoing request.                             | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | -                                                                    |
+| `access_control`        | list             | -       | Sets predefined [Access Control](#access-control) for `endpoint` block context.                                   | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | `access_control = ["foo"]`                                           |
+| `beta_scope`            | string or object | -       | Scope value required to use this endpoint (see [error type](../ERRORS.md#error-types) `beta_insufficient_scope`). | If the value is a string, the same scope value applies to all request methods. If there are different scope values for different request methods, use an object with the request methods as keys and string values. Methods not specified in this object are not permitted (see [error type](../ERRORS.md#error-types) `beta_operation_denied`). `"*"` is the key for "all other methods". A value `""` means "no (additional) scope required". | `beta_scope = "read"` or `beta_scope = { post = "write", "*" = "" }` |
+| `custom_log_fields`     | map              | -       | Defines log fields for [Custom Logging](LOGS.md#custom-logging).                                                  | &#9888; Inherited by nested blocks.                                                                                                                                                                                                                                                                                                                                                                                                             | -                                                                    |
+| [Modifiers](#modifiers) | -                | -       | -                                                                                                                 | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | -                                                                    |
+
+#### Endpoint Sequence
+
+If `request` and/or `proxy` block definitions are sequential based on their `backend_responses.*` variable references
+at load-time they will be executed sequentially. Unexpected responses can be caught with [error handling](ERRORS.md#endpoint-related-error_handler).
 
 ### Proxy Block
 
@@ -142,16 +148,17 @@ The `proxy` block creates and executes a proxy request to a backend service.
 &#9888; Multiple  `proxy` and [Request Block](#request-block)s are executed in parallel.
 <!-- TODO: shorten label text in table below and find better explanation for backend, backend reference or url - same for request block-->
 
-|Block name|Context|Label|Nested block(s)|
-| :-----------| :-----------| :-----------| :-----------|
-|`proxy`|[Endpoint Block](#endpoint-block)|&#9888; A `proxy` block or [Request Block](#request-block) w/o a label has an implicit label `"default"`. Only **one** `proxy` block or [Request Block](#request-block) w/ label `"default"` per [Endpoint Block](#endpoint-block) is allowed.|[Backend Block](#backend-block) (&#9888; required, if no [Backend Block](#backend-block) reference is defined or no `url` attribute is set.), [Websockets Block](#websockets-block) (&#9888; Either websockets attribute or block is allowed.)|
+| Block name | Context                           | Label                                                                                                                                                                                                                                          | Nested block(s)                                                                                                                                                                                                                                |
+|:-----------|:----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `proxy`    | [Endpoint Block](#endpoint-block) | &#9888; A `proxy` block or [Request Block](#request-block) w/o a label has an implicit label `"default"`. Only **one** `proxy` block or [Request Block](#request-block) w/ label `"default"` per [Endpoint Block](#endpoint-block) is allowed. | [Backend Block](#backend-block) (&#9888; required, if no [Backend Block](#backend-block) reference is defined or no `url` attribute is set.), [Websockets Block](#websockets-block) (&#9888; Either websockets attribute or block is allowed.) |
 
-| Attribute(s) | Type | Default | Description | Characteristic(s) | Example |
-| :----------- | :--- | :------ | :---------- | :---------------- | :------ |
-| `websockets` | bool | false | Allows support for websockets. This attribute is only allowed in the 'default' `proxy` block. Other `proxy` blocks, [Request Blocks](#request-block) or [Response Blocks](#response-block) are not allowed in the current [Endpoint Block](#endpoint-block). | &#9888; Either websockets attribute or block is allowed. | `websockets = true` |
-| `backend` |string|-|[Backend Block](#backend-block) reference, defined in [Definitions Block](#definitions-block)|&#9888; required, if no [Backend Block](#backend-block) or `url` attribute is defined.|`backend = "foo"`|
-| `url` |string|-|If defined, the host part of the URL must be the same as the `origin` attribute of the [Backend Block](#backend-block) (if defined).|-|-|
-|[Modifiers](#modifiers)|-|-|-|-|-|
+| Attribute(s)            | Type           | Default | Description                                                                                                                                                                                                                                                      | Characteristic(s)                                                                      | Example             |
+|:------------------------|:---------------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------|:--------------------|
+| `backend`               | string         | -       | [Backend Block](#backend-block) reference, defined in [Definitions Block](#definitions-block)                                                                                                                                                                    | &#9888; required, if no [Backend Block](#backend-block) or `url` attribute is defined. | `backend = "foo"`   |
+| `expected_status`       | list of number | -       | If defined, the response status code will be verified against this list of codes. If the status-code is unexpected an [`unexpected_status` error](./ERRORS.md#error-types) can be handled with an [`error_handler`](./ERRORS.md#endpoint-related-error_handler). | -                                                                                      | -                   |
+| `url`                   | string         | -       | If defined, the host part of the URL must be the same as the `origin` attribute of the [Backend Block](#backend-block) (if defined).                                                                                                                             | -                                                                                      | -                   |
+| `websockets`            | bool           | false   | Allows support for websockets. This attribute is only allowed in the 'default' `proxy` block. Other `proxy` blocks, [Request Blocks](#request-block) or [Response Blocks](#response-block) are not allowed in the current [Endpoint Block](#endpoint-block).     | &#9888; Either websockets attribute or block is allowed.                               | `websockets = true` |
+| [Modifiers](#modifiers) | -              | -       | -                                                                                                                                                                                                                                                                | -                                                                                      | -                   |
 
 ### Request Block
 
@@ -159,20 +166,23 @@ The `request` block creates and executes a request to a backend service.
 
 &#9888; Multiple [Proxy](#proxy-block) and `request` blocks are executed in parallel.
 
-|Block name|Context|Label|Nested block(s)|
-| :-----------| :-----------| :-----------| :-----------|
-|`request`| [Endpoint Block](#endpoint-block)|&#9888; A [Proxy Block](#proxy-block) or [Request Block](#request-block) w/o a label has an implicit label `"default"`. Only **one** [Proxy Block](#proxy-block) or [Request Block](#request-block) w/ label `"default"` per [Endpoint Block](#endpoint-block) is allowed.|[Backend Block](#backend-block) (&#9888; required, if no `backend` block reference is defined or no `url` attribute is set.|
+| Block name | Context                           | Label                                                                                                                                                                                                                                                                      | Nested block(s)                                                                                                             |
+|:-----------|:----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------|
+| `request`  | [Endpoint Block](#endpoint-block) | &#9888; A [Proxy Block](#proxy-block) or [Request Block](#request-block) w/o a label has an implicit label `"default"`. Only **one** [Proxy Block](#proxy-block) or [Request Block](#request-block) w/ label `"default"` per [Endpoint Block](#endpoint-block) is allowed. | [Backend Block](#backend-block) (&#9888; required, if no `backend` block reference is defined or no `url` attribute is set. |
+
 <!-- TODO: add available http methods -->
-| Attribute(s) | Type |Default|Description|Characteristic(s)| Example|
-| :------------------------------ | :--------------- | :--------------- | :--------------- | :--------------- | :--------------- |
-| `backend` |string|-|`backend` block reference, defined in [Definitions Block](#definitions-block)|&#9888; required, if no [Backend Block](#backend-block) is defined.|`backend = "foo"`|
-| `url` |string|-|-|If defined, the host part of the URL must be the same as the `origin` attribute of the used [Backend Block](#backend-block) or [Backend Block Reference](#backend-block) (if defined).|-|
-|`body`|string|-|-| Creates implicit default `Content-Type: text/plain` header field.|-|
-|`json_body`|null, bool, number, string, map, list|-|-|Creates implicit default `Content-Type: text/plain` header field.|-|
-| `form_body` |map|-|-|Creates implicit default `Content-Type: application/x-www-form-urlencoded` header field.|-|
-|`method`    |string|`GET`|-|-|-|
-|`headers`  |-|-|-|Same as `set_request_headers` in [Request Header](#request-header).|-|
-|`query_params`|-|-|-|Same as `set_query_params` in [Query Parameter](#query-parameter).|-|
+
+| Attribute(s)      | Type                                  | Default | Description                                                                                                                                                                                                                                                                                      | Characteristic(s)                                                                                                                                                                      | Example           |
+|:------------------|:--------------------------------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
+| `backend`         | string                                | -       | `backend` block reference, defined in [Definitions Block](#definitions-block)                                                                                                                                                                                                                    | &#9888; required, if no [Backend Block](#backend-block) is defined.                                                                                                                    | `backend = "foo"` |
+| `body`            | string                                | -       | -                                                                                                                                                                                                                                                                                                | Creates implicit default `Content-Type: text/plain` header field.                                                                                                                      | -                 |
+| `expected_status` | list<number>                          | -       | If defined, the response status code will be verified against this list of codes. If the status-code is unexpected an [`unexpected_status` error](./ERRORS.md#error-types) can be handled with an [`error_handler`](./ERRORS.md#endpoint-related-error_handler). | -                                                                                                                                                                                      | -                 |
+| `form_body`       | map                                   | -       | -                                                                                                                                                                                                                                                                                                | Creates implicit default `Content-Type: application/x-www-form-urlencoded` header field.                                                                                               | -                 |
+| `headers`         | -                                     | -       | -                                                                                                                                                                                                                                                                                                | Same as `set_request_headers` in [Request Header](#request-header).                                                                                                                    | -                 |
+| `json_body`       | null, bool, number, string, map, list | -       | -                                                                                                                                                                                                                                                                                                | Creates implicit default `Content-Type: text/plain` header field.                                                                                                                      | -                 |
+| `method`          | string                                | `GET`   | -                                                                                                                                                                                                                                                                                                | -                                                                                                                                                                                      | -                 |
+| `query_params`    | -                                     | -       | -                                                                                                                                                                                                                                                                                                | Same as `set_query_params` in [Query Parameter](#query-parameter).                                                                                                                     | -                 |
+| `url`             | string                                | -       | -                                                                                                                                                                                                                                                                                                | If defined, the host part of the URL must be the same as the `origin` attribute of the used [Backend Block](#backend-block) or [Backend Block Reference](#backend-block) (if defined). | -                 |
 
 ### Response Block
 
diff --git a/errors/couper.go b/errors/couper.go
index 550fbcbe..0735197f 100644
--- a/errors/couper.go
+++ b/errors/couper.go
@@ -13,8 +13,9 @@ var (
 	AccessControl     = &Error{synopsis: "access control error", kinds: []string{"access_control"}, httpStatus: http.StatusForbidden}
 	Backend           = &Error{synopsis: "backend error", httpStatus: http.StatusBadGateway}
 	BackendTimeout    = &Error{synopsis: "backend timeout error", httpStatus: http.StatusGatewayTimeout}
-	BackendValidation = &Error{synopsis: "backend validation error", httpStatus: http.StatusBadRequest}
+	BackendValidation = &Error{synopsis: "backend validation error", kinds: []string{"backend_validation"}, httpStatus: http.StatusBadRequest}
 	ClientRequest     = &Error{synopsis: "client request error", httpStatus: http.StatusBadRequest}
+	Endpoint          = &Error{synopsis: "endpoint error", kinds: []string{"endpoint"}, httpStatus: http.StatusBadGateway}
 	Evaluation        = &Error{synopsis: "expression evaluation error", kinds: []string{"evaluation"}, httpStatus: http.StatusInternalServerError}
 	Configuration     = &Error{synopsis: "configuration error", kinds: []string{"configuration"}, httpStatus: http.StatusInternalServerError}
 	Proxy             = &Error{synopsis: "proxy error", httpStatus: http.StatusBadGateway}
@@ -22,7 +23,6 @@ var (
 	RouteNotFound     = &Error{synopsis: "route not found error", httpStatus: http.StatusNotFound}
 	Server            = &Error{synopsis: "internal server error", httpStatus: http.StatusInternalServerError}
 	ServerShutdown    = &Error{synopsis: "server shutdown error", httpStatus: http.StatusInternalServerError}
-	ServerTimeout     = &Error{synopsis: "server timeout error", httpStatus: http.StatusGatewayTimeout}
 )
 
 func TypeToSnake(t interface{}) string {
diff --git a/errors/error.go b/errors/error.go
index 1c2ec953..a7960215 100644
--- a/errors/error.go
+++ b/errors/error.go
@@ -63,6 +63,14 @@ func (e *Error) Kinds() []string {
 	for i := len(e.kinds); i > 0; i-- {
 		reversed = append(reversed, e.kinds[i-1])
 	}
+
+	if eer, ok := e.inner.(*Error); ok {
+		k := eer.Kinds()
+		if len(k) > 0 {
+			reversed = append(reversed, k[0])
+		}
+	}
+
 	return reversed
 }
 
@@ -83,6 +91,9 @@ func (e *Error) Messagef(msg string, args ...interface{}) *Error {
 }
 
 func (e *Error) With(inner error) *Error {
+	if inner == nil {
+		return e
+	}
 	err := e.clone()
 	err.inner = inner
 	return err
diff --git a/errors/generate/types.go b/errors/generate/types.go
index 83703382..1aa52d73 100644
--- a/errors/generate/types.go
+++ b/errors/generate/types.go
@@ -5,13 +5,24 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"regexp"
 
 	"github.com/avenga/couper/errors"
 )
 
+var couperErrors []string
+
 func main() {
 	println("generating error types:\n")
 
+	// hold list of possible base kinds which can be skipped for type definitions
+	b, err := os.ReadFile(filepath.Join("errors", "couper.go"))
+	must(err)
+	result := regexp.MustCompile(`\t(\w+)\s+= &Error.+\n`).FindAllStringSubmatch(string(b), -1)
+	for _, r := range result {
+		couperErrors = append(couperErrors, r[1])
+	}
+
 	errTypes := map[string]int{}
 	sortedErrTypes := make([]string, len(errors.Definitions))
 	for idx, errImpl := range errors.Definitions {
@@ -45,12 +56,14 @@ var (
 	must(err)
 
 	for idx, kind := range sortedErrTypes {
+		if isDefined(kind) {
+			continue
+		}
 		_, err = io.WriteString(generated, fmt.Sprintf("\t%s = Definitions[%d]\n", errors.SnakeToCamel(kind), idx))
 		must(err)
 	}
 
-	_, err = io.WriteString(generated, fmt.Sprintf(`
-)
+	_, err = io.WriteString(generated, fmt.Sprintf(`)
 
 // typeDefinitions holds all related error definitions which are
 // catchable with an error_handler definition.
@@ -68,8 +81,7 @@ var types = typeDefinitions{
 		must(err)
 	}
 
-	_, err = io.WriteString(generated, fmt.Sprintf(`
-}
+	_, err = io.WriteString(generated, fmt.Sprintf(`}
 
 // IsKnown tells the configuration callee if Couper
 // has a defined error type with the given name.
@@ -84,6 +96,16 @@ func IsKnown(errorType string) bool {
 
 }
 
+// isDefined checks if the given typeName is defined and must be skipped for declaration with true result.
+func isDefined(typeName string) bool {
+	for _, t := range couperErrors {
+		if errors.SnakeToCamel(typeName) == t {
+			return true
+		}
+	}
+	return false
+}
+
 func must(err error) {
 	if err != nil {
 		println(err.Error())
diff --git a/errors/template.go b/errors/template.go
index bdafad85..d657d159 100644
--- a/errors/template.go
+++ b/errors/template.go
@@ -88,7 +88,7 @@ func (t *Template) WithContextFunc(fn http.HandlerFunc) *Template {
 
 }
 
-func (t *Template) ServeError(err error) http.Handler {
+func (t *Template) WithError(err error) http.Handler {
 	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 		rw.Header().Set("Content-Type", t.mime)
 
@@ -132,10 +132,10 @@ func (t *Template) ServeError(err error) http.Handler {
 		// fallback behaviour, execute internal template once
 		if tplErr != nil && (t != DefaultHTML && t != DefaultJSON) {
 			if !strings.Contains(t.mime, "text/html") {
-				DefaultJSON.ServeError(goErr).ServeHTTP(rw, req)
+				DefaultJSON.WithError(goErr).ServeHTTP(rw, req)
 				return
 			}
-			DefaultHTML.ServeError(goErr).ServeHTTP(rw, req)
+			DefaultHTML.WithError(goErr).ServeHTTP(rw, req)
 		} else if tplErr != nil && t.log != nil {
 			t.log.WithFields(data).Error(tplErr)
 		}
diff --git a/errors/type_defintions.go b/errors/type_defintions.go
index 16b4bc1f..4ababec7 100644
--- a/errors/type_defintions.go
+++ b/errors/type_defintions.go
@@ -5,6 +5,8 @@ import "net/http"
 // Definitions holds all implemented ones. The name must match the structs
 // snake-name for fallback purposes. See TypeToSnake usage and reference.
 var Definitions = []*Error{
+	AccessControl,
+
 	AccessControl.Kind("basic_auth").Status(http.StatusUnauthorized),
 	AccessControl.Kind("basic_auth").Kind("basic_auth_credentials_missing").Status(http.StatusUnauthorized),
 
@@ -21,4 +23,9 @@ var Definitions = []*Error{
 
 	AccessControl.Kind("beta_scope"),
 	AccessControl.Kind("beta_scope").Kind("beta_insufficient_scope"),
+
+	BackendValidation,
+
+	Endpoint.Kind("sequence"),
+	Endpoint.Kind("unexpected_status"),
 }
diff --git a/errors/types_generated.go b/errors/types_generated.go
index 86b7bda0..d2f18194 100644
--- a/errors/types_generated.go
+++ b/errors/types_generated.go
@@ -3,17 +3,19 @@
 package errors
 
 var (
-	BasicAuth                   = Definitions[0]
-	BasicAuthCredentialsMissing = Definitions[1]
-	Jwt                         = Definitions[2]
-	JwtTokenExpired             = Definitions[3]
-	JwtTokenInvalid             = Definitions[4]
-	JwtTokenMissing             = Definitions[5]
-	Oauth2                      = Definitions[6]
-	Saml2                       = Definitions[7]
-	BetaOperationDenied         = Definitions[8]
-	BetaScope                   = Definitions[9]
-	BetaInsufficientScope       = Definitions[10]
+	BasicAuth                   = Definitions[1]
+	BasicAuthCredentialsMissing = Definitions[2]
+	Jwt                         = Definitions[3]
+	JwtTokenExpired             = Definitions[4]
+	JwtTokenInvalid             = Definitions[5]
+	JwtTokenMissing             = Definitions[6]
+	Oauth2                      = Definitions[7]
+	Saml2                       = Definitions[8]
+	BetaOperationDenied         = Definitions[9]
+	BetaScope                   = Definitions[10]
+	BetaInsufficientScope       = Definitions[11]
+	Sequence                    = Definitions[13]
+	UnexpectedStatus            = Definitions[14]
 )
 
 // typeDefinitions holds all related error definitions which are
@@ -23,6 +25,7 @@ type typeDefinitions map[string]*Error
 // types holds all implemented ones. The name must match the structs
 // snake-name for fallback purposes. See TypeToSnake usage and reference.
 var types = typeDefinitions{
+	"access_control":                 AccessControl,
 	"basic_auth":                     BasicAuth,
 	"basic_auth_credentials_missing": BasicAuthCredentialsMissing,
 	"jwt":                            Jwt,
@@ -34,6 +37,9 @@ var types = typeDefinitions{
 	"beta_operation_denied":          BetaOperationDenied,
 	"beta_scope":                     BetaScope,
 	"beta_insufficient_scope":        BetaInsufficientScope,
+	"backend_validation":             BackendValidation,
+	"sequence":                       Sequence,
+	"unexpected_status":              UnexpectedStatus,
 }
 
 // IsKnown tells the configuration callee if Couper
diff --git a/eval/buffer.go b/eval/buffer.go
index a119d4f3..acea211c 100644
--- a/eval/buffer.go
+++ b/eval/buffer.go
@@ -7,7 +7,6 @@ import (
 	"strings"
 
 	"github.com/hashicorp/hcl/v2"
-	"github.com/hashicorp/hcl/v2/hclsyntax"
 
 	"github.com/avenga/couper/config/body"
 )
@@ -47,39 +46,21 @@ func MustBuffer(bodies ...hcl.Body) BufferOption {
 	}
 
 	var allExprs []hcl.Expression
-	var syntaxAttrs []hclsyntax.Attributes
-	// TODO: follow func call and their referenced remains
-	for _, b := range bodies {
-		if sb, ok := b.(*hclsyntax.Body); ok {
-			syntaxAttrs = append(syntaxAttrs, sb.Attributes)
-			for _, block := range sb.Blocks {
-				syntaxAttrs = append(syntaxAttrs, block.Body.Attributes)
-				if opt := bufferWithBlock(block.Type); opt != BufferNone {
-					result |= opt
-				}
-			}
-			continue
-		}
+	allAttributes := body.CollectAttributes(bodies...)
+	allBlockTypes := body.CollectBlockTypes(bodies...)
 
-		if all, ok := b.(body.Attributes); ok {
-			attrs := all.JustAllAttributes()
-			for _, attr := range attrs {
-				for _, v := range attr {
-					if opt := bufferWithAttribute(v.Name); opt != BufferNone {
-						result |= opt
-					}
-					allExprs = append(allExprs, v.Expr)
-				}
-			}
+	for _, blockType := range allBlockTypes {
+		if opt := bufferWithBlock(blockType); opt != BufferNone {
+			result |= opt
 		}
 	}
 
-	for _, attr := range syntaxAttrs {
-		for _, v := range attr {
-			if opt := bufferWithAttribute(v.Name); opt != BufferNone {
-				result |= opt
-			}
-			allExprs = append(allExprs, v.Expr)
+	// TODO: follow func call and their referenced remains
+	for _, attr := range allAttributes {
+		allExprs = append(allExprs, attr.Expr)
+
+		if opt := bufferWithAttribute(attr.Name); opt != BufferNone {
+			result |= opt
 		}
 	}
 
diff --git a/eval/context.go b/eval/context.go
index 2ae6a136..75009403 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -47,6 +47,7 @@ type Context struct {
 	jwtSigningConfigs map[string]*lib.JWTSigningConfig
 	saml              []*config.SAML
 	src               []byte
+	syncedVariables   *SyncedVariables
 }
 
 func NewContext(src []byte, defaults *config.Defaults) *Context {
@@ -105,9 +106,11 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {
 		oauth2:            c.oauth2[:],
 		jwtSigningConfigs: c.jwtSigningConfigs,
 		saml:              c.saml[:],
+		syncedVariables:   NewSyncedVariables(),
 	}
 
 	if rc := req.Context(); rc != nil {
+		rc = context.WithValue(rc, request.ContextVariablesSynced, ctx.syncedVariables)
 		ctx.inner = context.WithValue(rc, request.ContextType, ctx)
 	}
 
@@ -154,6 +157,9 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {
 		FormBody:  seetie.ValuesMapToValue(parseForm(req).PostForm),
 	}.Merge(newVariable(ctx.inner, req.Cookies(), req.Header))))
 
+	ctx.eval.Variables[BackendRequests] = cty.ObjectVal(make(map[string]cty.Value))
+	ctx.eval.Variables[BackendResponses] = cty.ObjectVal(make(map[string]cty.Value))
+
 	ctx.updateRequestRelatedFunctions(origin)
 	ctx.updateFunctions()
 
@@ -168,6 +174,7 @@ func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
 		oauth2:            c.oauth2[:],
 		jwtSigningConfigs: c.jwtSigningConfigs,
 		saml:              c.saml[:],
+		syncedVariables:   c.syncedVariables,
 	}
 	ctx.inner = context.WithValue(c.inner, request.ContextType, ctx)
 
@@ -178,53 +185,15 @@ func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
 			continue
 		}
 
-		bereq := beresp.Request
-		name := BackendDefault // TODO: name related error handling? override previous one for now
-		if n, ok := bereq.Context().Value(request.RoundTripName).(string); ok {
-			name = n
-		}
-
-		p := bereq.URL.Port()
-		if p == "" {
-			if bereq.URL.Scheme == "https" {
-				p = "443"
-			} else {
-				p = "80"
-			}
-		}
-		port, _ := strconv.ParseInt(p, 10, 64)
-
-		body, jsonBody := parseReqBody(bereq)
-		bereqs[name] = cty.ObjectVal(ContextMap{
-			Method:   cty.StringVal(bereq.Method),
-			URL:      cty.StringVal(bereq.URL.String()),
-			Origin:   cty.StringVal(NewRawOrigin(bereq.URL).String()),
-			Protocol: cty.StringVal(bereq.URL.Scheme),
-			Host:     cty.StringVal(bereq.URL.Hostname()),
-			Port:     cty.NumberIntVal(port),
-			Path:     cty.StringVal(bereq.URL.Path),
-			Query:    seetie.ValuesMapToValue(bereq.URL.Query()),
-			Body:     body,
-			JsonBody: jsonBody,
-			FormBody: seetie.ValuesMapToValue(parseForm(bereq).PostForm),
-		}.Merge(newVariable(ctx.inner, bereq.Cookies(), bereq.Header)))
-
-		var respBody, respJsonBody cty.Value
-		if !IsUpgradeResponse(bereq, beresp) {
-			bufferOption, ok := bereq.Context().Value(request.BufferOptions).(BufferOption)
-			if ok && (bufferOption&BufferResponse) == BufferResponse {
-				respBody, respJsonBody = parseRespBody(beresp)
-			}
-		}
-		resps[name] = cty.ObjectVal(ContextMap{
-			HttpStatus: cty.NumberIntVal(int64(beresp.StatusCode)),
-			JsonBody:   respJsonBody,
-			Body:       respBody,
-		}.Merge(newVariable(ctx.inner, beresp.Cookies(), beresp.Header)))
+		name, bereqVal, berespVal := newBerespValues(ctx, false, beresp)
+		bereqs[name] = bereqVal
+		resps[name] = berespVal
 	}
 
-	ctx.eval.Variables[BackendRequests] = cty.ObjectVal(bereqs)
-	ctx.eval.Variables[BackendResponses] = cty.ObjectVal(resps)
+	// Prevent overriding existing variables with successive calls to this method.
+	// Could happen with error_handler within an endpoint. Merge them.
+	c.updateBackendVariables(ctx.eval, BackendRequests, bereqs)
+	c.updateBackendVariables(ctx.eval, BackendResponses, resps)
 
 	clientOrigin, _ := seetie.ValueToMap(ctx.eval.Variables[ClientRequest])[Origin].(string)
 	originUrl, _ := url.Parse(clientOrigin)
@@ -234,6 +203,66 @@ func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
 	return ctx
 }
 
+func newBerespValues(ctx context.Context, readBody bool, beresp *http.Response) (name string, bereqVal cty.Value, berespVal cty.Value) {
+	bereq := beresp.Request
+
+	name = "default"
+	if n, ok := bereq.Context().Value(request.RoundTripName).(string); ok {
+		name = n
+	}
+
+	p := bereq.URL.Port()
+	if p == "" {
+		if bereq.URL.Scheme == "https" {
+			p = "443"
+		} else {
+			p = "80"
+		}
+	}
+	port, _ := strconv.ParseInt(p, 10, 64)
+
+	body, jsonBody := parseReqBody(bereq)
+	bereqVal = cty.ObjectVal(ContextMap{
+		Method:   cty.StringVal(bereq.Method),
+		URL:      cty.StringVal(bereq.URL.String()),
+		Origin:   cty.StringVal(NewRawOrigin(bereq.URL).String()),
+		Protocol: cty.StringVal(bereq.URL.Scheme),
+		Host:     cty.StringVal(bereq.URL.Hostname()),
+		Port:     cty.NumberIntVal(port),
+		Path:     cty.StringVal(bereq.URL.Path),
+		Query:    seetie.ValuesMapToValue(bereq.URL.Query()),
+		Body:     body,
+		JsonBody: jsonBody,
+		FormBody: seetie.ValuesMapToValue(parseForm(bereq).PostForm),
+	}.Merge(newVariable(ctx, bereq.Cookies(), bereq.Header)))
+
+	var respBody, respJsonBody cty.Value
+	if readBody && !IsUpgradeResponse(bereq, beresp) {
+		bufferOption, ok := bereq.Context().Value(request.BufferOptions).(BufferOption)
+		if ok && (bufferOption&BufferResponse) == BufferResponse {
+			respBody, respJsonBody = parseRespBody(beresp)
+		}
+	}
+	berespVal = cty.ObjectVal(ContextMap{
+		HttpStatus: cty.NumberIntVal(int64(beresp.StatusCode)),
+		JsonBody:   respJsonBody,
+		Body:       respBody,
+	}.Merge(newVariable(ctx, beresp.Cookies(), beresp.Header)))
+
+	return name, bereqVal, berespVal
+}
+
+func (c *Context) updateBackendVariables(evalCtx *hcl.EvalContext, key string, cmap ContextMap) {
+	if !evalCtx.Variables[key].IsNull() && evalCtx.Variables[key].LengthInt() > 0 {
+		merged, _ := lib.Merge([]cty.Value{evalCtx.Variables[key], cty.ObjectVal(cmap)})
+		if !merged.IsNull() {
+			evalCtx.Variables[key] = merged
+		}
+	} else {
+		evalCtx.Variables[key] = cty.ObjectVal(cmap)
+	}
+}
+
 // WithJWTSigningConfigs initially sets up the lib.FnJWTSign function.
 func (c *Context) WithJWTSigningConfigs(configs map[string]*lib.JWTSigningConfig) *Context {
 	c.jwtSigningConfigs = configs
@@ -279,6 +308,17 @@ func (c *Context) HCLContext() *hcl.EvalContext {
 	return c.eval
 }
 
+func (c *Context) HCLContextSync() *hcl.EvalContext {
+	if c.syncedVariables == nil {
+		return c.eval
+	}
+
+	e := c.cloneEvalContext()
+	c.syncedVariables.Sync(e.Variables)
+
+	return e
+}
+
 func (c *Context) getCodeVerifier() (*pkce.CodeVerifier, error) {
 	cv, ok := c.memorize[lib.CodeVerifier]
 	var err error
diff --git a/eval/http.go b/eval/http.go
index 3e4698ef..82c276ee 100644
--- a/eval/http.go
+++ b/eval/http.go
@@ -2,7 +2,6 @@ package eval
 
 import (
 	"bytes"
-	"context"
 	"io"
 	"net/http"
 	"net/url"
@@ -41,6 +40,8 @@ const (
 	attrSetResHeaders = "set_response_headers"
 	attrAddResHeaders = "add_response_headers"
 	attrDelResHeaders = "remove_response_headers"
+
+	attrCustomLogFields = "custom_log_fields"
 )
 
 // SetGetBody determines if we have to buffer a request body for further processing.
@@ -99,16 +100,11 @@ func SetBody(req *http.Request, body []byte) {
 	parseForm(req)
 }
 
-func ApplyRequestContext(ctx context.Context, body hcl.Body, req *http.Request) error {
+func ApplyRequestContext(httpCtx *hcl.EvalContext, body hcl.Body, req *http.Request) error {
 	if req == nil {
 		return nil
 	}
 
-	var httpCtx *hcl.EvalContext
-	if c, ok := ctx.Value(request.ContextType).(*Context); ok {
-		httpCtx = c.HCLContext()
-	}
-
 	headerCtx := req.Header
 
 	attrs, err := getAllAttributes(body)
@@ -330,6 +326,10 @@ func IsUpgradeResponse(req *http.Request, res *http.Response) bool {
 	return upgradeType(req.Header) == upgradeType(res.Header)
 }
 
+var customLogFieldsSchema = &hcl.BodySchema{Attributes: []hcl.AttributeSchema{
+	{Name: attrCustomLogFields},
+}}
+
 func ApplyCustomLogs(httpCtx *hcl.EvalContext, bodies []hcl.Body, logger *logrus.Entry) logrus.Fields {
 	var values []cty.Value
 
@@ -338,16 +338,20 @@ func ApplyCustomLogs(httpCtx *hcl.EvalContext, bodies []hcl.Body, logger *logrus
 			continue // Test cases
 		}
 
-		bodyContent, _, _ := body.PartialContent(config.BackendInlineSchema)
+		bodyContent, _, _ := body.PartialContent(customLogFieldsSchema)
 
-		if logs, ok := bodyContent.Attributes["custom_log_fields"]; ok {
-			val, err := Value(httpCtx, logs.Expr)
-			if err != nil {
-				logger.Debug(err)
-			} else {
-				values = append(values, val)
-			}
+		logs, ok := bodyContent.Attributes[attrCustomLogFields]
+		if !ok {
+			continue
+		}
+
+		val, err := Value(httpCtx, logs.Expr)
+		if err != nil {
+			logger.Debug(err)
+			continue
 		}
+
+		values = append(values, val)
 	}
 
 	val, err := lib.Merge(values)
@@ -358,7 +362,7 @@ func ApplyCustomLogs(httpCtx *hcl.EvalContext, bodies []hcl.Body, logger *logrus
 	return seetie.ValueToLogFields(val)
 }
 
-func ApplyResponseContext(ctx context.Context, body hcl.Body, beresp *http.Response) error {
+func ApplyResponseContext(ctx *hcl.EvalContext, body hcl.Body, beresp *http.Response) error {
 	if beresp == nil {
 		return nil
 	}
@@ -380,12 +384,7 @@ func ApplyResponseContext(ctx context.Context, body hcl.Body, beresp *http.Respo
 	return nil
 }
 
-func ApplyResponseStatus(ctx context.Context, attr *hcl.Attribute, beresp *http.Response) (int, error) {
-	var httpCtx *hcl.EvalContext
-	if c, ok := ctx.Value(request.ContextType).(*Context); ok {
-		httpCtx = c.HCLContext()
-	}
-
+func ApplyResponseStatus(httpCtx *hcl.EvalContext, attr *hcl.Attribute, beresp *http.Response) (int, error) {
 	statusValue, err := Value(httpCtx, attr.Expr)
 	if err != nil {
 		return 0, err
@@ -400,7 +399,7 @@ func ApplyResponseStatus(ctx context.Context, attr *hcl.Attribute, beresp *http.
 	if beresp != nil {
 		if status == 204 {
 			beresp.Request.Context().
-				Value(request.LogEntry).(*logrus.Entry).WithContext(ctx).
+				Value(request.LogEntry).(*logrus.Entry).
 				Warn("set_response_status: removing body, if any due to status-code 204")
 
 			beresp.Body = io.NopCloser(bytes.NewBuffer([]byte{}))
@@ -414,12 +413,7 @@ func ApplyResponseStatus(ctx context.Context, attr *hcl.Attribute, beresp *http.
 	return int(status), nil
 }
 
-func ApplyResponseHeaderOps(ctx context.Context, body hcl.Body, headers ...http.Header) error {
-	var httpCtx *hcl.EvalContext
-	if c, ok := ctx.Value(request.ContextType).(*Context); ok {
-		httpCtx = c.HCLContext()
-	}
-
+func ApplyResponseHeaderOps(httpCtx *hcl.EvalContext, body hcl.Body, headers ...http.Header) error {
 	attrs, err := getAllAttributes(body)
 	if err != nil {
 		return err
diff --git a/eval/sync.go b/eval/sync.go
new file mode 100644
index 00000000..a809336f
--- /dev/null
+++ b/eval/sync.go
@@ -0,0 +1,55 @@
+package eval
+
+import (
+	"github.com/zclconf/go-cty/cty"
+	"net/http"
+	"sync"
+)
+
+type SyncedVariables struct {
+	items sync.Map
+}
+
+func NewSyncedVariables() *SyncedVariables {
+	return &SyncedVariables{
+		items: sync.Map{},
+	}
+}
+
+type syncPair struct {
+	name          string
+	bereq, beresp cty.Value
+}
+
+// Set finalized cty req/resp pair.
+func (sv *SyncedVariables) Set(beresp *http.Response) {
+	name, bereqV, berespV := newBerespValues(beresp.Request.Context(), true, beresp)
+	sv.items.Store(name, &syncPair{
+		name:   name,
+		bereq:  bereqV,
+		beresp: berespV,
+	})
+}
+
+func (sv *SyncedVariables) Sync(variables map[string]cty.Value) {
+	bereqs := variables[BackendRequests].AsValueMap()
+	beresps := variables[BackendResponses].AsValueMap()
+
+	sv.items.Range(func(key, value interface{}) bool {
+		p := value.(*syncPair)
+		if bereqs == nil {
+			bereqs = make(map[string]cty.Value)
+		}
+		bereqs[p.name] = p.bereq
+
+		if beresps == nil {
+			beresps = make(map[string]cty.Value)
+		}
+		beresps[p.name] = p.beresp
+
+		return true
+	})
+
+	variables[BackendRequests] = cty.ObjectVal(bereqs)
+	variables[BackendResponses] = cty.ObjectVal(beresps)
+}
diff --git a/handler/endpoint.go b/handler/endpoint.go
index 250fbe98..6fdf4533 100644
--- a/handler/endpoint.go
+++ b/handler/endpoint.go
@@ -6,7 +6,6 @@ import (
 	"net/http"
 	"runtime/debug"
 	"strconv"
-	"strings"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/sirupsen/logrus"
@@ -33,18 +32,21 @@ type Endpoint struct {
 
 type EndpointOptions struct {
 	APIName        string
+	BufferOpts     eval.BufferOption
 	Context        hcl.Body
-	Error          *errors.Template
+	ErrorTemplate  *errors.Template
+	ErrorHandler   http.Handler
+	IsErrorHandler bool
 	LogHandlerKind string
 	LogPattern     string
 	ReqBodyLimit   int64
-	BufferOpts     eval.BufferOption
 	ServerOpts     *server.Options
 
-	Proxies  producer.Roundtrips
-	Redirect *producer.Redirect
-	Requests producer.Roundtrips
-	Response *producer.Response
+	Proxies   producer.Roundtrips
+	Redirect  *producer.Redirect
+	Requests  producer.Roundtrips
+	Sequences producer.Sequences
+	Response  *producer.Response
 }
 
 type BodyLimit interface {
@@ -61,55 +63,40 @@ func NewEndpoint(opts *EndpointOptions, log *logrus.Entry, modifier []hcl.Body)
 }
 
 func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+	reqCtx := e.withContext(req)
+
 	var (
-		clientres    *http.Response
-		err          error
-		log          = e.log.WithContext(req.Context())
-		isErrHandler = strings.HasPrefix(e.opts.LogHandlerKind, "error_") // weak ref
+		clientres *http.Response
+		err       error
+		log       = e.log.WithContext(reqCtx)
 	)
 
-	// Bind some values for logging purposes
-	reqCtx := context.WithValue(req.Context(), request.Endpoint, e.opts.LogPattern)
-	reqCtx = context.WithValue(reqCtx, request.EndpointKind, e.opts.LogHandlerKind)
-	reqCtx = context.WithValue(reqCtx, request.APIName, e.opts.APIName)
-	reqCtx = context.WithValue(reqCtx, request.BufferOptions, e.opts.BufferOpts)
-	*req = *req.WithContext(reqCtx)
-	if e.opts.LogPattern != "" {
-		span := trace.SpanFromContext(reqCtx)
-		span.SetAttributes(telemetry.KeyEndpoint.String(e.opts.LogPattern))
-	}
-
 	defer func() {
-		rc := recover()
-		if rc != nil {
+		if rc := recover(); rc != nil {
 			log.WithField("panic", string(debug.Stack())).Error(rc)
 			if clientres == nil {
-				e.opts.Error.ServeError(errors.Server).ServeHTTP(rw, req)
+				e.opts.ErrorTemplate.WithError(errors.Server).ServeHTTP(rw, req)
 			}
 		}
 	}()
 
-	// subCtx is handled by this endpoint handler and should not be attached to req
-	subCtx, cancel := context.WithCancel(reqCtx)
-	defer cancel()
+	if e.opts.LogPattern != "" {
+		span := trace.SpanFromContext(reqCtx)
+		span.SetAttributes(telemetry.KeyEndpoint.String(e.opts.LogPattern))
+	}
 
-	if ee := eval.ApplyRequestContext(reqCtx, e.opts.Context, req); ee != nil {
-		e.opts.Error.ServeError(ee).ServeHTTP(rw, req)
+	if ee := eval.ApplyRequestContext(eval.ContextFromRequest(req).HCLContext(), e.opts.Context, req); ee != nil {
+		e.opts.ErrorTemplate.WithError(ee).ServeHTTP(rw, req)
 		return
 	}
 
-	proxyResults := make(producer.Results, e.opts.Proxies.Len())
-	requestResults := make(producer.Results, e.opts.Requests.Len())
-
-	// go for it due to chan write on error
-	go e.opts.Proxies.Produce(subCtx, req, proxyResults)
-	go e.opts.Requests.Produce(subCtx, req, requestResults)
+	// subCtx is handled by this endpoint handler and should not be attached to req
+	subCtx, cancel := context.WithCancel(reqCtx)
+	defer cancel()
 
-	beresps := make(producer.ResultMap)
-	// TODO: read parallel, proxy first for now
-	e.readResults(subCtx, proxyResults, beresps)
-	e.readResults(subCtx, requestResults, beresps)
+	beresps, err := e.produce(req.WithContext(subCtx))
 
+	// check for client cancels before reading backend response bodies
 	select {
 	case <-reqCtx.Done():
 		err = reqCtx.Err()
@@ -118,98 +105,32 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	default:
 	}
 
-	evalContext := eval.ContextFromRequest(req)
-	evalContext = evalContext.WithBeresps(beresps.List()...)
-
-	customLogEvalCtxCh, ok := req.Context().Value(request.LogCustomEvalResult).(chan *eval.Context)
-	if ok {
-		select {
-		case <-reqCtx.Done():
-			err = reqCtx.Err()
-			log.WithError(errors.ClientRequest.With(err)).Error()
+	// handle errors first before entering the happy path
+	if !e.opts.IsErrorHandler {
+		if handled := e.handleError(rw, req, err); handled {
 			return
-		case customLogEvalCtxCh <- evalContext:
 		}
 	}
 
-	// assume prio or err on conf load if set with response
+	// assume configured priority, prefer redirect to response and default ones
 	if e.opts.Redirect != nil {
 		clientres = e.newRedirect()
 	} else if e.opts.Response != nil {
-		// TODO: refactor with error_handler, catch at least panics for now
-		for _, b := range beresps {
-			if b.Err == nil {
-				continue
-			}
-
-			switch b.Err.(type) {
-			case producer.ResultPanic:
-				log.WithError(b.Err).Error()
-			}
-
-			if b.Err != nil {
-				err = b.Err
-				break
-			}
-		}
-
-		if err == nil {
-			_, span := telemetry.NewSpanFromContext(subCtx, "response", trace.WithSpanKind(trace.SpanKindProducer))
-			defer span.End()
-			clientres, err = producer.NewResponse(req, e.opts.Response.Context, evalContext, http.StatusOK)
-		}
-	} else {
-		if result, ok := beresps["default"]; ok {
-			clientres = result.Beresp
-			err = result.Err
-		} else {
-			// fallback
-			err = errors.Configuration
-
-			if isErrHandler {
-				err, _ = req.Context().Value(request.Error).(*errors.Error)
-			} else {
-				// TODO determine error priority, may solved with error_handler
-				// on roundtrip panic the context label is missing atm
-				// pick the first err from beresps
-				for _, br := range beresps {
-					if br != nil && br.Err != nil {
-						err = br.Err
-						break
-					}
-				}
-			}
+		_, span := telemetry.NewSpanFromContext(subCtx, "response", trace.WithSpanKind(trace.SpanKindProducer))
+		defer span.End()
+		clientres, err = producer.NewResponse(req, e.opts.Response.Context, http.StatusOK)
+	} else if result, exist := beresps["default"]; exist {
+		clientres = result.Beresp
+		err = result.Err
+	} else if e.opts.IsErrorHandler && err == nil {
+		var ok bool
+		err, ok = req.Context().Value(request.Error).(error)
+		if !ok {
+			err = errors.Server
 		}
 	}
 
-	if err != nil {
-		serveErr := err
-		switch err.(type) { // TODO proper err mapping and handling
-		case net.Error:
-			serveErr = errors.Request.With(err)
-			if p, ok := req.Context().Value(request.RoundTripProxy).(bool); ok && p {
-				serveErr = errors.Proxy.With(err)
-			}
-		case producer.ResultPanic:
-			serveErr = errors.Server.With(err)
-			log.WithError(err).Error()
-		}
-
-		content, _, _ := e.opts.Context.PartialContent(config.Endpoint{}.Schema(true))
-		errFromCtx := req.Context().Value(request.Error)
-		if attr, ok := content.Attributes["set_response_status"]; isErrHandler && errFromCtx == err && ok {
-			if statusCode, applyErr := eval.ApplyResponseStatus(evalContext, attr, nil); statusCode > 0 {
-				if serr, k := serveErr.(*errors.Error); k {
-					serveErr = serr.Status(statusCode)
-				} else {
-					serveErr = errors.Server.With(serveErr).Status(statusCode)
-				}
-			} else if applyErr != nil {
-				e.log.WithError(applyErr)
-			}
-		}
-
-		e.opts.Error.ServeError(serveErr).ServeHTTP(rw, req)
+	if handled := e.handleError(rw, req, err); handled {
 		return
 	}
 
@@ -219,6 +140,8 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	default:
 	}
 
+	httpCtx := eval.ContextFromRequest(req).HCLContextSync()
+
 	w, ok := rw.(*writer.Response)
 	if !ok {
 		log.Errorf("response writer: type error")
@@ -228,13 +151,13 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 			return
 		}
 
-		w.AddModifier(evalContext, e.modifier...)
+		w.AddModifier(httpCtx, e.modifier...)
 		rw = w
 	}
 
 	// always apply before write: redirect, response
-	if err = eval.ApplyResponseContext(evalContext, e.opts.Context, clientres); err != nil {
-		e.opts.Error.ServeError(err).ServeHTTP(rw, req)
+	if err = eval.ApplyResponseContext(httpCtx, e.opts.Context, clientres); err != nil {
+		e.opts.ErrorTemplate.WithError(err).ServeHTTP(rw, req)
 		return
 	}
 
@@ -257,6 +180,16 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	_ = clientres.Body.Close()
 }
 
+// withContext binds some endpoint context related values for logging and buffer purposes.
+func (e *Endpoint) withContext(req *http.Request) context.Context {
+	reqCtx := context.WithValue(req.Context(), request.Endpoint, e.opts.LogPattern)
+	reqCtx = context.WithValue(reqCtx, request.EndpointKind, e.opts.LogHandlerKind)
+	reqCtx = context.WithValue(reqCtx, request.APIName, e.opts.APIName)
+	reqCtx = context.WithValue(reqCtx, request.BufferOptions, e.opts.BufferOpts)
+	*req = *req.WithContext(reqCtx)
+	return reqCtx
+}
+
 func (e *Endpoint) newRedirect() *http.Response {
 	// TODO use http.RedirectHandler
 	status := http.StatusMovedPermanently
@@ -267,12 +200,43 @@ func (e *Endpoint) newRedirect() *http.Response {
 	}
 }
 
-func (e *Endpoint) readResults(ctx context.Context, requestResults producer.Results, beresps producer.ResultMap) {
+// produce hands over all possible outgoing requests to the producer interface and reads
+// the backend response results afterwards. Returns first occurred backend error.
+func (e *Endpoint) produce(req *http.Request) (producer.ResultMap, error) {
+	results := make(producer.ResultMap)
+
+	trips := []producer.Roundtrips{e.opts.Proxies, e.opts.Requests, e.opts.Sequences}
+	tripCh := make(chan chan *producer.Result, len(trips))
+	for _, trip := range trips {
+		resultCh := make(chan *producer.Result, trip.Len())
+		go func(rt producer.Roundtrips, rc chan *producer.Result) {
+			rt.Produce(req, resultCh)
+			close(rc)
+		}(trip, resultCh)
+		tripCh <- resultCh
+	}
+	close(tripCh)
+
+	for resultCh := range tripCh {
+		e.readResults(resultCh, results)
+	}
+
+	var err error // TODO: prefer default resp err
+	// TODO: additionally log all panic error types
+	for _, r := range results {
+		if r.Err != nil {
+			err = r.Err
+			break
+		}
+	}
+
+	return results, err
+}
+
+func (e *Endpoint) readResults(requestResults producer.Results, beresps producer.ResultMap) {
 	i := 0
 	for {
 		select {
-		case <-ctx.Done():
-			return
 		case r, more := <-requestResults:
 			if !more {
 				return
@@ -290,6 +254,52 @@ func (e *Endpoint) readResults(ctx context.Context, requestResults producer.Resu
 	}
 }
 
+func (e *Endpoint) handleError(rw http.ResponseWriter, req *http.Request, err error) bool {
+	if err == nil {
+		return false
+	}
+
+	ctxErr := req.Context().Value(request.Error)
+	serveErr := err
+	switch err.(type) {
+	case net.Error:
+		serveErr = errors.Request.With(err)
+		if p, ok := req.Context().Value(request.RoundTripProxy).(bool); ok && p {
+			serveErr = errors.Proxy.With(err)
+		}
+	case producer.ResultPanic:
+		serveErr = errors.Server.With(err)
+	}
+
+	if e.opts.ErrorHandler != nil {
+		if ctxErr == nil {
+			ctxErr = serveErr
+			*req = *req.WithContext(context.WithValue(req.Context(), request.Error, ctxErr))
+		}
+		e.opts.ErrorHandler.ServeHTTP(rw, req)
+		return true
+	}
+
+	content, _, _ := e.opts.Context.PartialContent(config.Endpoint{}.Schema(true))
+
+	// modify response status code if set
+	if attr, ok := content.Attributes["set_response_status"]; e.opts.IsErrorHandler && ctxErr == err && ok {
+		if statusCode, applyErr := eval.
+			ApplyResponseStatus(eval.ContextFromRequest(req).HCLContextSync(), attr, nil); statusCode > 0 {
+			if serr, k := serveErr.(*errors.Error); k {
+				serveErr = serr.Status(statusCode)
+			} else {
+				serveErr = errors.Server.With(serveErr).Status(statusCode)
+			}
+		} else if applyErr != nil {
+			e.log.WithError(applyErr)
+		}
+	}
+
+	e.opts.ErrorTemplate.WithError(serveErr).ServeHTTP(rw, req)
+	return true
+}
+
 func (e *Endpoint) Options() *server.Options {
 	return e.opts.ServerOpts
 }
diff --git a/handler/error.go b/handler/error.go
index e01992af..5449773e 100644
--- a/handler/error.go
+++ b/handler/error.go
@@ -25,12 +25,12 @@ func NewErrorHandler(kindsHandler map[string]http.Handler, errTpl *errors.Templa
 func (e *Error) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	err, ok := req.Context().Value(request.Error).(*errors.Error)
 	if !ok { // all errors within this context should have this type, otherwise an implementation error
-		e.template.ServeError(errors.Server).ServeHTTP(rw, req)
+		e.template.WithError(errors.Server).ServeHTTP(rw, req)
 		return
 	}
 
 	if e.kindsHandler == nil { // nothing defined, just serve err with template
-		e.template.ServeError(err).ServeHTTP(rw, req)
+		e.template.WithError(err).ServeHTTP(rw, req)
 		return
 	}
 
@@ -60,5 +60,5 @@ func (e *Error) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	}
 
 	// fallback with no matching error handler
-	e.template.ServeError(err).ServeHTTP(rw, req)
+	e.template.WithError(err).ServeHTTP(rw, req)
 }
diff --git a/handler/file.go b/handler/file.go
index 0add3002..2bc2cd94 100644
--- a/handler/file.go
+++ b/handler/file.go
@@ -73,7 +73,7 @@ func (f *File) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 	file, info, err := f.openDocRootFile(reqPath)
 	if err != nil {
-		f.srvOptions.FilesErrTpl.ServeError(errors.RouteNotFound).ServeHTTP(rw, req)
+		f.srvOptions.FilesErrTpl.WithError(errors.RouteNotFound).ServeHTTP(rw, req)
 		return
 	}
 	defer file.Close()
@@ -85,7 +85,7 @@ func (f *File) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 	if r, ok := rw.(*writer.Response); ok {
 		evalContext := eval.ContextFromRequest(req)
-		r.AddModifier(evalContext, f.modifier...)
+		r.AddModifier(evalContext.HCLContext(), f.modifier...)
 	}
 
 	http.ServeContent(rw, req, reqPath, info.ModTime(), file)
@@ -93,14 +93,14 @@ func (f *File) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 func (f *File) serveDirectory(reqPath string, rw http.ResponseWriter, req *http.Request) {
 	if !f.HasResponse(req) {
-		f.srvOptions.FilesErrTpl.ServeError(errors.RouteNotFound).ServeHTTP(rw, req)
+		f.srvOptions.FilesErrTpl.WithError(errors.RouteNotFound).ServeHTTP(rw, req)
 		return
 	}
 
 	if !strings.HasSuffix(reqPath, "/") {
 		if r, ok := rw.(*writer.Response); ok {
 			evalContext := eval.ContextFromRequest(req)
-			r.AddModifier(evalContext, f.modifier...)
+			r.AddModifier(evalContext.HCLContext(), f.modifier...)
 		}
 
 		rw.Header().Set("Location", utils.JoinPath(req.URL.Path, "/"))
@@ -112,14 +112,14 @@ func (f *File) serveDirectory(reqPath string, rw http.ResponseWriter, req *http.
 
 	file, info, err := f.openDocRootFile(reqPath)
 	if err != nil || info.IsDir() {
-		f.srvOptions.FilesErrTpl.ServeError(errors.RouteNotFound).ServeHTTP(rw, req)
+		f.srvOptions.FilesErrTpl.WithError(errors.RouteNotFound).ServeHTTP(rw, req)
 		return
 	}
 	defer file.Close()
 
 	if r, ok := rw.(*writer.Response); ok {
 		evalContext := eval.ContextFromRequest(req)
-		r.AddModifier(evalContext, f.modifier...)
+		r.AddModifier(evalContext.HCLContext(), f.modifier...)
 	}
 
 	http.ServeContent(rw, req, reqPath, info.ModTime(), file)
diff --git a/handler/middleware/custom_logs.go b/handler/middleware/custom_logs.go
index 90438b13..8970978b 100644
--- a/handler/middleware/custom_logs.go
+++ b/handler/middleware/custom_logs.go
@@ -8,7 +8,6 @@ import (
 	"github.com/hashicorp/hcl/v2"
 
 	"github.com/avenga/couper/config/request"
-	"github.com/avenga/couper/eval"
 )
 
 var _ http.Handler = &CustomLogs{}
@@ -30,12 +29,7 @@ func NewCustomLogsHandler(bodies []hcl.Body, next http.Handler, handlerName stri
 func (c *CustomLogs) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	var bodies []hcl.Body
 
-	if b := req.Context().Value(request.LogCustomAccess); b != nil {
-		bodies = b.([]hcl.Body)
-	}
-
 	ctx := context.WithValue(req.Context(), request.LogCustomAccess, append(bodies, c.bodies...))
-	ctx = context.WithValue(ctx, request.LogCustomEvalResult, make(chan *eval.Context, 2))
 	*req = *req.WithContext(ctx)
 
 	c.next.ServeHTTP(rw, req)
diff --git a/handler/middleware/uid.go b/handler/middleware/uid.go
index f9aedfed..2a41377c 100644
--- a/handler/middleware/uid.go
+++ b/handler/middleware/uid.go
@@ -45,7 +45,7 @@ func (u *UID) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	}
 
 	if err != nil {
-		errors.DefaultHTML.ServeError(errors.ClientRequest.With(err)).ServeHTTP(rw, req)
+		errors.DefaultHTML.WithError(errors.ClientRequest.With(err)).ServeHTTP(rw, req)
 		return
 	}
 
diff --git a/handler/producer/produce.go b/handler/producer/produce.go
index 56f9e43a..b2796350 100644
--- a/handler/producer/produce.go
+++ b/handler/producer/produce.go
@@ -1,16 +1,17 @@
 package producer
 
 import (
-	"context"
 	"net/http"
 )
 
 var (
 	_ Roundtrips = Proxies{}
 	_ Roundtrips = Requests{}
+	_ Roundtrips = Sequences{}
+	_ Roundtrips = Sequence{}
 )
 
 type Roundtrips interface {
-	Produce(ctx context.Context, req *http.Request, results chan<- *Result)
+	Produce(req *http.Request, results chan<- *Result)
 	Len() int
 }
diff --git a/handler/producer/proxy.go b/handler/producer/proxy.go
index 5fb8aec4..180f67fd 100644
--- a/handler/producer/proxy.go
+++ b/handler/producer/proxy.go
@@ -20,10 +20,10 @@ type Proxy struct {
 
 type Proxies []*Proxy
 
-func (pr Proxies) Produce(ctx context.Context, clientReq *http.Request, results chan<- *Result) {
+func (pr Proxies) Produce(clientReq *http.Request, results chan<- *Result) {
 	var currentName string // at least pre roundtrip
 	wg := &sync.WaitGroup{}
-
+	ctx := clientReq.Context()
 	var rootSpan trace.Span
 	if len(pr) > 0 {
 		ctx, rootSpan = telemetry.NewSpanFromContext(ctx, "proxies", trace.WithSpanKind(trace.SpanKindProducer))
@@ -60,10 +60,7 @@ func (pr Proxies) Produce(ctx context.Context, clientReq *http.Request, results
 		rootSpan.End()
 	}
 
-	go func() {
-		wg.Wait()
-		close(results)
-	}()
+	wg.Wait()
 }
 
 func (pr Proxies) Len() int {
diff --git a/handler/producer/request.go b/handler/producer/request.go
index bb4abf24..dadc06b0 100644
--- a/handler/producer/request.go
+++ b/handler/producer/request.go
@@ -29,13 +29,12 @@ type Request struct {
 // Requests represents the producer <Requests> object.
 type Requests []*Request
 
-func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<- *Result) {
+func (r Requests) Produce(req *http.Request, results chan<- *Result) {
 	var currentName string // at least pre roundtrip
 	wg := &sync.WaitGroup{}
-	roundtripCreated := false
-
+	ctx := req.Context()
 	var rootSpan trace.Span
-	if len(r) > 0 {
+	if r.Len() > 0 {
 		ctx, rootSpan = telemetry.NewSpanFromContext(ctx, "requests", trace.WithSpanKind(trace.SpanKindProducer))
 	}
 
@@ -48,15 +47,10 @@ func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<-
 				},
 				RoundTripName: currentName,
 			}
-
-			if !roundtripCreated {
-				close(results)
-			}
 		}
 	}()
 
-	evalctx := ctx.Value(request.ContextType).(*eval.Context)
-	updated := evalctx.WithClientRequest(req)
+	hclCtx := eval.ContextFromRequest(req).HCLContextSync() // also synced for requests due to sequence case
 
 	for _, or := range r {
 		// span end by result reader
@@ -70,21 +64,21 @@ func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<-
 
 		var method, url string
 
-		methodVal, err := eval.ValueFromAttribute(updated.HCLContext(), bodyContent, "method")
+		methodVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "method")
 		if err != nil {
 			results <- &Result{Err: err}
 			continue
 		}
 		method = seetie.ValueToString(methodVal)
 
-		urlVal, err := eval.ValueFromAttribute(updated.HCLContext(), bodyContent, "url")
+		urlVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "url")
 		if err != nil {
 			results <- &Result{Err: err}
 			continue
 		}
 		url = seetie.ValueToString(urlVal)
 
-		body, defaultContentType, err := eval.GetBody(updated.HCLContext(), bodyContent)
+		body, defaultContentType, err := eval.GetBody(hclCtx, bodyContent)
 		if err != nil {
 			results <- &Result{Err: err}
 			continue
@@ -110,6 +104,14 @@ func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<-
 			continue
 		}
 
+		expStatusVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "expected_status")
+		if err != nil {
+			results <- &Result{Err: err}
+			continue
+		}
+
+		outCtx = context.WithValue(outCtx, request.EndpointExpectedStatus, seetie.ValueToIntSlice(expStatusVal))
+
 		if defaultContentType != "" {
 			outreq.Header.Set("Content-Type", defaultContentType)
 		}
@@ -117,7 +119,7 @@ func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<-
 		eval.SetBody(outreq, []byte(body))
 
 		*outreq = *outreq.WithContext(outCtx)
-		err = eval.ApplyRequestContext(outCtx, or.Context, outreq)
+		err = eval.ApplyRequestContext(hclCtx, or.Context, outreq)
 		if err != nil {
 			results <- &Result{Err: err}
 			continue
@@ -125,7 +127,6 @@ func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<-
 
 		span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(outreq)...)
 
-		roundtripCreated = true
 		wg.Add(1)
 		go roundtrip(or.Backend, outreq, results, wg)
 	}
@@ -134,10 +135,7 @@ func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<-
 		rootSpan.End()
 	}
 
-	go func() {
-		wg.Wait()
-		close(results)
-	}()
+	wg.Wait()
 }
 
 func (r Requests) Len() int {
diff --git a/handler/producer/response.go b/handler/producer/response.go
index 89a356e8..3819e008 100644
--- a/handler/producer/response.go
+++ b/handler/producer/response.go
@@ -18,7 +18,7 @@ type Response struct {
 	Context hcl.Body
 }
 
-func NewResponse(req *http.Request, resp hcl.Body, evalCtx *eval.Context, statusCode int) (*http.Response, error) {
+func NewResponse(req *http.Request, resp hcl.Body, statusCode int) (*http.Response, error) {
 	clientres := &http.Response{
 		Header:     make(http.Header),
 		Proto:      req.Proto,
@@ -27,7 +27,7 @@ func NewResponse(req *http.Request, resp hcl.Body, evalCtx *eval.Context, status
 		Request:    req,
 	}
 
-	hclCtx := evalCtx.HCLContext()
+	hclCtx := eval.ContextFromRequest(req).HCLContextSync()
 
 	content, _, diags := resp.PartialContent(config.ResponseInlineSchema)
 	if diags.HasErrors() {
diff --git a/handler/producer/result.go b/handler/producer/result.go
index a2da6117..684e1bb2 100644
--- a/handler/producer/result.go
+++ b/handler/producer/result.go
@@ -66,6 +66,26 @@ func roundtrip(rt http.RoundTripper, req *http.Request, results chan<- *Result,
 	beresp, err := rt.RoundTrip(req)
 	span.End()
 
+	if expStatus, ok := req.Context().Value(request.EndpointExpectedStatus).([]int64); beresp != nil &&
+		ok && len(expStatus) > 0 {
+		var seen bool
+		for _, exp := range expStatus {
+			if beresp.StatusCode == int(exp) {
+				seen = true
+				break
+			}
+		}
+
+		if !seen {
+			results <- &Result{
+				Beresp:        beresp,
+				Err:           errors.UnexpectedStatus.With(err),
+				RoundTripName: rtn,
+			}
+			return
+		}
+	}
+
 	results <- &Result{
 		Beresp:        beresp,
 		Err:           err,
diff --git a/handler/producer/sequence.go b/handler/producer/sequence.go
new file mode 100644
index 00000000..94b1a2c1
--- /dev/null
+++ b/handler/producer/sequence.go
@@ -0,0 +1,114 @@
+package producer
+
+import (
+	"net/http"
+
+	"github.com/hashicorp/hcl/v2"
+	"go.opentelemetry.io/otel/trace"
+
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/telemetry"
+)
+
+type SequenceItem struct {
+	Backend http.RoundTripper
+	Context hcl.Body
+	Name    string // label
+}
+
+// Sequence represents a list of serialized items.
+type Sequence []*SequenceItem
+
+// Sequences holds several list of serialized items.
+type Sequences []Sequence
+
+func (seqs Sequences) Produce(req *http.Request, results chan<- *Result) {
+	var rootSpan trace.Span
+	ctx := req.Context()
+	if len(seqs) > 0 {
+		ctx, rootSpan = telemetry.NewSpanFromContext(ctx, "sequences", trace.WithSpanKind(trace.SpanKindProducer))
+	}
+
+	resultsCh := make(chan *Result, seqs.Len())
+
+	for _, s := range seqs {
+		go s.Produce(req, resultsCh)
+	}
+
+	for i := 0; i < seqs.Len(); i++ {
+		results <- <-resultsCh
+	}
+
+	if rootSpan != nil {
+		rootSpan.End()
+	}
+}
+
+func (seqs Sequences) Len() int {
+	return len(seqs)
+}
+
+func (s Sequence) Produce(req *http.Request, results chan<- *Result) {
+	var rootSpan trace.Span
+
+	ctx := req.Context()
+	if len(s) > 0 {
+		ctx, rootSpan = telemetry.NewSpanFromContext(ctx, "sequence", trace.WithSpanKind(trace.SpanKindProducer))
+	}
+	defer func() {
+		if rootSpan != nil {
+			rootSpan.End()
+		}
+	}()
+
+	result := make(chan *Result, 1)
+
+	var lastResult *Result
+	var lastBeresps []*http.Response
+	var moreEntries bool
+	for _, seq := range s {
+		outreq := req.WithContext(ctx)
+
+		if seq.Context == nil {
+			Proxies{&Proxy{Name: seq.Name,
+				RoundTrip: seq.Backend,
+			}}.Produce(outreq, result)
+		} else {
+			Requests{&Request{
+				Backend: seq.Backend,
+				Context: seq.Context,
+				Name:    seq.Name,
+			}}.Produce(outreq, result)
+		}
+
+		select {
+		case <-outreq.Context().Done():
+			return
+		case lastResult, moreEntries = <-result:
+			if !moreEntries {
+				return
+			}
+		}
+
+		if lastResult.Err != nil {
+			lastResult.Err = errors.Sequence.With(lastResult.Err)
+			results <- lastResult
+			return
+		}
+
+		lastBeresps = append(lastBeresps, lastResult.Beresp)
+	}
+
+	if lastResult == nil {
+		results <- &Result{Err: errors.Sequence.With(errors.New().Message("no result"))}
+	}
+
+	results <- lastResult
+}
+
+func (s Sequence) Len() int {
+	if len(s) > 0 {
+		return 1
+	}
+	return 0
+}
diff --git a/handler/proxy.go b/handler/proxy.go
index 36c5e69f..33198f93 100644
--- a/handler/proxy.go
+++ b/handler/proxy.go
@@ -51,8 +51,10 @@ func (p *Proxy) RoundTrip(req *http.Request) (*http.Response, error) {
 		req.Header.Del(key)
 	}
 
+	hclCtx := eval.ContextFromRequest(req).HCLContextSync()
+
 	// 2. Apply proxy-body
-	if err := eval.ApplyRequestContext(req.Context(), p.context, req); err != nil {
+	if err := eval.ApplyRequestContext(hclCtx, p.context, req); err != nil {
 		return nil, err
 	}
 
@@ -61,15 +63,23 @@ func (p *Proxy) RoundTrip(req *http.Request) (*http.Response, error) {
 		return nil, err
 	}
 
-	urlVal, err := eval.ValueFromBodyAttribute(eval.ContextFromRequest(req).HCLContext(), p.context, "url")
+	// 4. apply some hcl context
+	expStatusVal, err := eval.ValueFromBodyAttribute(hclCtx, p.context, "expected_status")
+	if err != nil {
+		return nil, err
+	}
+
+	outCtx := context.WithValue(req.Context(), request.EndpointExpectedStatus, seetie.ValueToIntSlice(expStatusVal))
+
+	urlVal, err := eval.ValueFromBodyAttribute(hclCtx, p.context, "url")
 	if err != nil {
 		return nil, err
 	}
 
 	if url := seetie.ValueToString(urlVal); url != "" {
-		ctx := context.WithValue(req.Context(), request.URLAttribute, url)
-		*req = *req.WithContext(ctx)
+		outCtx = context.WithValue(outCtx, request.URLAttribute, url)
 	}
+	*req = *req.WithContext(outCtx)
 
 	if err = p.registerWebsocketsResponse(req); err != nil {
 		return nil, err
@@ -121,7 +131,7 @@ func (p *Proxy) RoundTrip(req *http.Request) (*http.Response, error) {
 	transport.RemoveHopHeaders(beresp.Header)
 
 	evalCtx := eval.ContextFromRequest(req)
-	err = eval.ApplyResponseContext(evalCtx.WithBeresps(beresp), p.context, beresp)
+	err = eval.ApplyResponseContext(evalCtx.HCLContextSync(), p.context, beresp)
 
 	return beresp, err
 }
@@ -145,6 +155,8 @@ func (p *Proxy) applyWebsocketsRequest(req *http.Request) error {
 	ctx = context.WithValue(ctx, request.WebsocketsAllowed, true)
 	*req = *req.WithContext(ctx)
 
+	hclCtx := eval.ContextFromRequest(req).HCLContextSync()
+
 	// This method needs the 'request.WebsocketsAllowed' flag in the 'req.context'.
 	if !eval.IsUpgradeRequest(req) {
 		return nil
@@ -159,7 +171,7 @@ func (p *Proxy) applyWebsocketsRequest(req *http.Request) error {
 	if diags.HasErrors() {
 		return diags
 	}
-	if err := eval.ApplyRequestContext(req.Context(), wsBody, req); err != nil {
+	if err := eval.ApplyRequestContext(hclCtx, wsBody, req); err != nil {
 		return err
 	}
 
@@ -168,7 +180,7 @@ func (p *Proxy) applyWebsocketsRequest(req *http.Request) error {
 		return nil
 	}
 
-	val, err := eval.Value(eval.ContextFromRequest(req).HCLContext(), attr.Expr)
+	val, err := eval.Value(hclCtx, attr.Expr)
 	if err != nil {
 		return err
 	}
@@ -205,7 +217,7 @@ func (p *Proxy) registerWebsocketsResponse(req *http.Request) error {
 	evalCtx := eval.ContextFromRequest(req)
 
 	if rw, ok := req.Context().Value(request.ResponseWriter).(*writer.Response); ok {
-		rw.AddModifier(evalCtx, wsBody, p.context)
+		rw.AddModifier(evalCtx.HCLContextSync(), wsBody, p.context)
 	}
 
 	return nil
diff --git a/handler/spa.go b/handler/spa.go
index aece2499..8dd19af5 100644
--- a/handler/spa.go
+++ b/handler/spa.go
@@ -45,24 +45,24 @@ func (s *Spa) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	file, err := os.Open(s.file)
 	if err != nil {
 		if _, ok := err.(*os.PathError); ok {
-			s.srvOptions.ServerErrTpl.ServeError(errors.RouteNotFound).ServeHTTP(rw, req)
+			s.srvOptions.ServerErrTpl.WithError(errors.RouteNotFound).ServeHTTP(rw, req)
 			return
 		}
 
-		s.srvOptions.ServerErrTpl.ServeError(errors.Configuration).ServeHTTP(rw, req)
+		s.srvOptions.ServerErrTpl.WithError(errors.Configuration).ServeHTTP(rw, req)
 		return
 	}
 	defer file.Close()
 
 	fileInfo, err := file.Stat()
 	if err != nil || fileInfo.IsDir() {
-		s.srvOptions.ServerErrTpl.ServeError(errors.Configuration).ServeHTTP(rw, req)
+		s.srvOptions.ServerErrTpl.WithError(errors.Configuration).ServeHTTP(rw, req)
 		return
 	}
 
 	if r, ok := rw.(*writer.Response); ok {
 		evalContext := eval.ContextFromRequest(req)
-		r.AddModifier(evalContext, s.modifier...)
+		r.AddModifier(evalContext.HCLContext(), s.modifier...)
 	}
 
 	http.ServeContent(rw, req, s.file, fileInfo.ModTime(), file)
diff --git a/handler/transport/backend.go b/handler/transport/backend.go
index b0266d18..42777256 100644
--- a/handler/transport/backend.go
+++ b/handler/transport/backend.go
@@ -78,13 +78,15 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 	ctx := context.WithValue(req.Context(), request.LogCustomUpstream, []hcl.Body{b.context})
 	*req = *req.WithContext(ctx)
 
+	hclCtx := eval.ContextFromRequest(req).HCLContextSync()
+
 	// Execute before <b.evalTransport()> due to right
 	// handling of query-params in the URL attribute.
-	if err := eval.ApplyRequestContext(req.Context(), b.context, req); err != nil {
+	if err := eval.ApplyRequestContext(hclCtx, b.context, req); err != nil {
 		return nil, err
 	}
 
-	tc, err := b.evalTransport(req)
+	tc, err := b.evalTransport(hclCtx, req)
 	if err != nil {
 		return nil, err
 	}
@@ -134,7 +136,13 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 	}
 
 	if err != nil {
-		return nil, err
+		berespErr := &http.Response{
+			Request: req,
+		} // provide outreq (variable) on error cases
+		if varSync, ok := req.Context().Value(request.ContextVariablesSynced).(*eval.SyncedVariables); ok {
+			varSync.Set(berespErr)
+		}
+		return berespErr, err
 	}
 
 	if !eval.IsUpgradeResponse(req, beresp) {
@@ -153,15 +161,10 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 	// from this result.
 	evalCtx := eval.ContextFromRequest(req)
 	evalCtx = evalCtx.WithBeresps(beresp)
-	err = eval.ApplyResponseContext(evalCtx, b.context, beresp)
+	err = eval.ApplyResponseContext(evalCtx.HCLContext(), b.context, beresp)
 
-	customLogEvalCtxCh, ok := req.Context().Value(request.LogCustomEvalResult).(chan *eval.Context)
-	if ok {
-		select {
-		case <-req.Context().Done():
-			return beresp, err
-		case customLogEvalCtxCh <- evalCtx:
-		}
+	if varSync, ok := req.Context().Value(request.ContextVariablesSynced).(*eval.SyncedVariables); ok {
+		varSync.Set(beresp)
 	}
 
 	return beresp, err
@@ -301,12 +304,7 @@ func (b *Backend) withTimeout(req *http.Request) <-chan error {
 	return errCh
 }
 
-func (b *Backend) evalTransport(req *http.Request) (*Config, error) {
-	var httpContext *hcl.EvalContext
-	if httpCtx, ok := req.Context().Value(request.ContextType).(*eval.Context); ok {
-		httpContext = httpCtx.HCLContext()
-	}
-
+func (b *Backend) evalTransport(httpCtx *hcl.EvalContext, req *http.Request) (*Config, error) {
 	log := b.upstreamLog.LogEntry()
 
 	bodyContent, _, diags := b.context.PartialContent(config.BackendInlineSchema)
@@ -324,7 +322,7 @@ func (b *Backend) evalTransport(req *http.Request) (*Config, error) {
 		{"hostname", &hostname},
 		{"proxy", &proxyURL},
 	} {
-		if v, err := eval.ValueFromAttribute(httpContext, bodyContent, p.attrName); err != nil {
+		if v, err := eval.ValueFromAttribute(httpCtx, bodyContent, p.attrName); err != nil {
 			log.WithError(errors.Evaluation.Label(b.name).With(err)).Error()
 		} else if v != cty.NilVal {
 			*p.target = seetie.ValueToString(v)
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index 26aad25f..304812b7 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -203,6 +203,21 @@ func ValueToStringSlice(src cty.Value) []string {
 	return l
 }
 
+func ValueToIntSlice(src cty.Value) []int64 {
+	var n []int64
+	if !src.IsKnown() || src.IsNull() || !src.CanIterateElements() {
+		return n
+	}
+
+	for _, s := range src.AsValueSlice() {
+		if !s.IsKnown() {
+			continue
+		}
+		n = append(n, ValueToInt(s))
+	}
+	return n
+}
+
 var whitespaceRegex = regexp.MustCompile(`^\s*$`)
 
 // ValueToString explicitly drops all other (unknown) types and
diff --git a/logging/access_log.go b/logging/access_log.go
index 2a5209bd..f89dd84f 100644
--- a/logging/access_log.go
+++ b/logging/access_log.go
@@ -170,9 +170,10 @@ func (log *AccessLog) Do(writer http.ResponseWriter, req *http.Request) {
 }
 
 func UpdateCustomAccessLogContext(req *http.Request, body hcl.Body) {
+	bodies := []hcl.Body{body}
 	if b := req.Context().Value(request.LogCustomAccess); b != nil {
-		bodies, _ := b.([]hcl.Body)
-		bodies = append(bodies, body)
-		*req = *req.WithContext(context.WithValue(req.Context(), request.LogCustomAccess, bodies))
+		previousBodies, _ := b.([]hcl.Body)
+		bodies = append(previousBodies, bodies...)
 	}
+	*req = *req.WithContext(context.WithValue(req.Context(), request.LogCustomAccess, bodies))
 }
diff --git a/logging/hooks/custom_logs.go b/logging/hooks/custom_logs.go
index f82e1233..e4417bd2 100644
--- a/logging/hooks/custom_logs.go
+++ b/logging/hooks/custom_logs.go
@@ -53,21 +53,9 @@ func (c *CustomLogs) Fire(entry *logrus.Entry) error {
 }
 
 func fire(entry *logrus.Entry, bodyKey request.ContextKey) {
-	var evalCtx *eval.Context
-
-	customEvalCtxCh, ok := entry.Context.Value(request.LogCustomEvalResult).(chan *eval.Context)
-	if ok {
-		select {
-		case evalCtx = <-customEvalCtxCh:
-		default: // pass through, e.g. on early errors we will not receive something useful
-		}
-	}
-
-	if evalCtx == nil {
-		evalCtx, ok = entry.Context.Value(request.ContextType).(*eval.Context)
-		if !ok {
-			return
-		}
+	evalCtx, ok := entry.Context.Value(request.ContextType).(*eval.Context)
+	if !ok {
+		return
 	}
 
 	bodies := entry.Context.Value(bodyKey)
@@ -80,7 +68,7 @@ func fire(entry *logrus.Entry, bodyKey request.ContextKey) {
 		return
 	}
 
-	if fields := eval.ApplyCustomLogs(evalCtx.HCLContext(), hclBodies, entry); len(fields) > 0 {
+	if fields := eval.ApplyCustomLogs(evalCtx.HCLContextSync(), hclBodies, entry); len(fields) > 0 {
 		entry.Data[customLogField] = fields
 	}
 }
diff --git a/logging/upstream_log.go b/logging/upstream_log.go
index 3284a5ae..a6033efd 100644
--- a/logging/upstream_log.go
+++ b/logging/upstream_log.go
@@ -70,42 +70,42 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) {
 	fields["request"] = requestFields
 
 	oCtx, openAPIContext := validation.NewWithContext(req.Context())
-	*req = *req.WithContext(httptrace.WithClientTrace(oCtx, clientTrace))
+	outreq := req.WithContext(httptrace.WithClientTrace(oCtx, clientTrace))
 
 	rtStart := time.Now()
-	beresp, err := u.next.RoundTrip(req)
+	beresp, err := u.next.RoundTrip(outreq)
 	rtDone := time.Now()
 
-	if req.Host != "" {
-		requestFields["origin"] = req.Host
-		requestFields["host"], requestFields["port"] = splitHostPort(req.Host)
+	if outreq.Host != "" {
+		requestFields["origin"] = outreq.Host
+		requestFields["host"], requestFields["port"] = splitHostPort(outreq.Host)
 		if requestFields["port"] == "" {
 			delete(requestFields, "port")
 		}
 	}
 
 	path := &url.URL{
-		Path:       req.URL.Path,
-		RawPath:    req.URL.RawPath,
-		RawQuery:   req.URL.RawQuery,
-		ForceQuery: req.URL.ForceQuery,
-		Fragment:   req.URL.Fragment,
+		Path:       outreq.URL.Path,
+		RawPath:    outreq.URL.RawPath,
+		RawQuery:   outreq.URL.RawQuery,
+		ForceQuery: outreq.URL.ForceQuery,
+		Fragment:   outreq.URL.Fragment,
 	}
 	requestFields["path"] = path.String()
-	requestFields["headers"] = filterHeader(u.config.RequestHeaders, req.Header)
+	requestFields["headers"] = filterHeader(u.config.RequestHeaders, outreq.Header)
 
-	fields["url"] = req.URL.String()
+	fields["url"] = outreq.URL.String()
 
-	if req.URL.User != nil && req.URL.User.Username() != "" {
-		fields["auth_user"] = req.URL.User.Username()
-	} else if user, _, ok := req.BasicAuth(); ok && user != "" {
+	if outreq.URL.User != nil && outreq.URL.User.Username() != "" {
+		fields["auth_user"] = outreq.URL.User.Username()
+	} else if user, _, ok := outreq.BasicAuth(); ok && user != "" {
 		fields["auth_user"] = user
 	}
 
-	if tr, ok := req.Context().Value(request.TokenRequest).(string); ok && tr != "" {
+	if tr, ok := outreq.Context().Value(request.TokenRequest).(string); ok && tr != "" {
 		fields["token_request"] = tr
 
-		if retries, ok := req.Context().Value(request.TokenRequestRetries).(uint8); ok && retries > 0 {
+		if retries, ok := outreq.Context().Value(request.TokenRequestRetries).(uint8); ok && retries > 0 {
 			fields["token_request_retry"] = retries
 		}
 	}
@@ -135,7 +135,7 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) {
 	fields["timings"] = timingResults
 	//timings["ttlb"] = roundMS(rtDone.Sub(timeTTFB)) // TODO: depends on stream or buffer
 
-	entry := u.log.WithFields(logrus.Fields(fields)).WithContext(req.Context())
+	entry := u.log.WithFields(logrus.Fields(fields)).WithContext(outreq.Context())
 	entry.Time = startTime
 
 	if err != nil {
diff --git a/server/http.go b/server/http.go
index 30dac9e7..65bbf13c 100644
--- a/server/http.go
+++ b/server/http.go
@@ -186,14 +186,14 @@ func (s *HTTPServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	req.Host = s.getHost(req)
 	host, _, err := runtime.GetHostPort(req.Host)
 	if err != nil {
-		h = errors.DefaultHTML.ServeError(errors.ClientRequest)
+		h = errors.DefaultHTML.WithError(errors.ClientRequest)
 	}
 
 	mux, ok := s.muxers[host]
 	if !ok {
 		mux, ok = s.muxers["*"]
 		if !ok && h == nil {
-			h = errors.DefaultHTML.ServeError(errors.Configuration)
+			h = errors.DefaultHTML.WithError(errors.Configuration)
 		}
 	}
 
@@ -211,7 +211,7 @@ func (s *HTTPServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	}
 
 	if err = s.setGetBody(h, req); err != nil {
-		h = mux.opts.ServerOptions.ServerErrTpl.ServeError(err)
+		h = mux.opts.ServerOptions.ServerErrTpl.WithError(err)
 	}
 
 	req.URL.Host = req.Host
diff --git a/server/mux.go b/server/mux.go
index 7e1b134d..0334bf0e 100644
--- a/server/mux.go
+++ b/server/mux.go
@@ -143,7 +143,7 @@ func (m *Mux) FindHandler(req *http.Request) http.Handler {
 		// and handle api related errors accordingly.
 		// Otherwise look for existing files or spa fallback.
 		if tpl, api := m.getAPIErrorTemplate(req.URL.Path); tpl != nil {
-			return tpl.ServeError(errors.RouteNotFound.Label(api.BasePath)) // TODO: api label
+			return tpl.WithError(errors.RouteNotFound.Label(api.BasePath)) // TODO: api label
 		}
 
 		fileHandler, exist := m.hasFileResponse(req)
@@ -159,7 +159,7 @@ func (m *Mux) FindHandler(req *http.Request) http.Handler {
 			}
 
 			// Fallback
-			return m.opts.ServerOptions.ServerErrTpl.ServeError(errors.RouteNotFound)
+			return m.opts.ServerOptions.ServerErrTpl.WithError(errors.RouteNotFound)
 		}
 	}
 
diff --git a/server/testdata/integration/logging/01_couper.hcl b/server/testdata/integration/logs/03_couper.hcl
similarity index 100%
rename from server/testdata/integration/logging/01_couper.hcl
rename to server/testdata/integration/logs/03_couper.hcl
diff --git a/server/writer/response.go b/server/writer/response.go
index 66438d53..d0fecdf4 100644
--- a/server/writer/response.go
+++ b/server/writer/response.go
@@ -22,7 +22,7 @@ type writer interface {
 }
 
 type modifier interface {
-	AddModifier(*eval.Context, ...hcl.Body)
+	AddModifier(*hcl.EvalContext, ...hcl.Body)
 }
 
 var (
@@ -46,7 +46,7 @@ type Response struct {
 	rawBytesWritten int
 	bytesWritten    int
 	// modifier
-	evalCtx  *eval.Context
+	evalCtx  *hcl.EvalContext
 	modifier []hcl.Body
 }
 
@@ -182,7 +182,7 @@ func (r *Response) WrittenBytes() int {
 	return r.bytesWritten
 }
 
-func (r *Response) AddModifier(evalCtx *eval.Context, modifier ...hcl.Body) {
+func (r *Response) AddModifier(evalCtx *hcl.EvalContext, modifier ...hcl.Body) {
 	r.evalCtx = evalCtx
 	r.modifier = append(r.modifier, modifier...)
 }
@@ -193,6 +193,6 @@ func (r *Response) applyModifier() {
 	}
 
 	for _, body := range r.modifier {
-		eval.ApplyResponseHeaderOps(r.evalCtx, body, r.Header())
+		_ = eval.ApplyResponseHeaderOps(r.evalCtx, body, r.Header())
 	}
 }


Test patch:
diff --git a/config/configload/merged_test.go b/config/body/merged_test.go
similarity index 94%
rename from config/configload/merged_test.go
rename to config/body/merged_test.go
index 76ac4fbf..b5dffd0c 100644
--- a/config/configload/merged_test.go
+++ b/config/body/merged_test.go
@@ -1,4 +1,4 @@
-package configload_test
+package body_test
 
 import (
 	"testing"
@@ -10,7 +10,7 @@ import (
 	"github.com/zclconf/go-cty/cty"
 
 	"github.com/avenga/couper/config"
-	"github.com/avenga/couper/config/configload"
+	"github.com/avenga/couper/config/body"
 	"github.com/avenga/couper/eval"
 )
 
@@ -83,7 +83,7 @@ block {
 	}
 
 	schema, _ = gohcl.ImpliedBodySchema(bodies.Block[0])
-	content, _, diags = configload.MergeBodies(hclBodies[:]).PartialContent(schema)
+	content, _, diags = body.MergeBodies(hclBodies[:]...).PartialContent(schema)
 	if diags.HasErrors() {
 		t.Error(diags)
 	}
diff --git a/config/configload/endpoint_test.go b/config/configload/endpoint_test.go
new file mode 100644
index 00000000..6ea10d7c
--- /dev/null
+++ b/config/configload/endpoint_test.go
@@ -0,0 +1,15 @@
+package configload
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/avenga/couper/config"
+)
+
+func Test_refineEndpoints_noPattern(t *testing.T) {
+	err := refineEndpoints(nil, config.Endpoints{{Pattern: ""}}, true)
+	if err == nil || !strings.HasSuffix(err.Error(), "endpoint: missing path pattern; ") {
+		t.Errorf("refineEndpoints() error = %v, wantErr: endpoint: missing path pattern ", err)
+	}
+}
diff --git a/config/configload/load_test.go b/config/configload/validate_test.go
similarity index 89%
rename from config/configload/load_test.go
rename to config/configload/validate_test.go
index fa1455ba..b4a1d95d 100644
--- a/config/configload/load_test.go
+++ b/config/configload/validate_test.go
@@ -2,23 +2,14 @@ package configload
 
 import (
 	"context"
-	"strings"
 	"testing"
 
 	"github.com/hashicorp/hcl/v2"
 
-	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/runtime"
 	logrustest "github.com/sirupsen/logrus/hooks/test"
 )
 
-func Test_refineEndpoints_noPattern(t *testing.T) {
-	err := refineEndpoints(nil, config.Endpoints{{Pattern: ""}}, true)
-	if err == nil || !strings.HasSuffix(err.Error(), "endpoint: missing path pattern; ") {
-		t.Errorf("refineEndpoints() error = %v, wantErr: endpoint: missing path pattern ", err)
-	}
-}
-
 func Test_VerifyBodyAttributes(t *testing.T) {
 	type testCase struct {
 		name    string
@@ -192,13 +183,13 @@ func TestLabels(t *testing.T) {
 				_, err = runtime.NewServerConfiguration(conf, log, nil)
 			}
 
-			var error = ""
+			var errMsg string
 			if err != nil {
-				error = err.Error()
+				errMsg = err.Error()
 			}
 
-			if tt.error != error {
-				subT.Errorf("%q: Unexpected configuration error:\n\tWant: %q\n\tGot:  %q", tt.name, tt.error, error)
+			if tt.error != errMsg {
+				subT.Errorf("%q: Unexpected configuration error:\n\tWant: %q\n\tGot:  %q", tt.name, tt.error, errMsg)
 			}
 		})
 	}
diff --git a/errors/template_test.go b/errors/template_test.go
index f763e391..9b7afd3b 100644
--- a/errors/template_test.go
+++ b/errors/template_test.go
@@ -27,7 +27,7 @@ func TestTemplate_ServeError(t1 *testing.T) {
 		t1.Run(tt.name, func(subT *testing.T) {
 			rec := writer.NewResponseWriter(httptest.NewRecorder(), "")
 			req := httptest.NewRequest(http.MethodGet, "/", nil)
-			errors.DefaultJSON.ServeError(tt.err).ServeHTTP(rec, req)
+			errors.DefaultJSON.WithError(tt.err).ServeHTTP(rec, req)
 
 			rec.Flush()
 
diff --git a/eval/lib/jwt_test.go b/eval/lib/jwt_test.go
index 68db4b3d..be263f02 100644
--- a/eval/lib/jwt_test.go
+++ b/eval/lib/jwt_test.go
@@ -663,26 +663,26 @@ func TestJwtSignDynamic(t *testing.T) {
 			helper.Must(err)
 
 			if resultClaims["exp"] == nil {
-				subT.Errorf("Expected exp claim, got: %#v", body)
+				subT.Errorf("Expected exp claim, got: %s", body)
 			}
-			exp := resultClaims["exp"].(float64)
+			exp, _ := resultClaims["exp"].(float64)
 			if !fuzzyEqual(int64(exp)-now, tt.wantTTL, 1) {
-				subT.Errorf(string(body))
+				subT.Error(string(body))
 				subT.Errorf("Expected %d, got: %d", tt.wantTTL, int64(exp)-now)
 			}
 			if resultClaims["x-method"] == nil {
-				subT.Errorf("Expected x-method claim, got: %#v", body)
+				subT.Errorf("Expected x-method claim, got: %s", body)
 			}
 			if resultClaims["x-method"] != tt.wantMeth {
 				subT.Errorf("Expected: %s, got: %s", tt.wantMeth, resultClaims["x-method"])
 			}
 
 			if resultClaims["x-status"] == nil {
-				subT.Errorf("Expected x-status claim, got: %#v", body)
+				subT.Errorf("Expected x-status claim, got: %s", body)
 			}
-			status := int64(resultClaims["x-status"].(float64))
+			status, _ := resultClaims["x-status"].(float64)
 			if status != 200 {
-				subT.Errorf("Expected: %d, got: %d", http.StatusOK, status)
+				subT.Errorf("Expected: %d, got: %d", http.StatusOK, int64(status))
 			}
 		})
 	}
diff --git a/handler/endpoint_test.go b/handler/endpoint_test.go
index bbe845cb..35fc4212 100644
--- a/handler/endpoint_test.go
+++ b/handler/endpoint_test.go
@@ -87,9 +87,9 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 				&transport.Config{NoProxyFromEnv: true}, nil, logger)
 
 			ep := handler.NewEndpoint(&handler.EndpointOptions{
-				Error:        errors.DefaultJSON,
-				Context:      remain.Inline,
-				ReqBodyLimit: 1024,
+				ErrorTemplate: errors.DefaultJSON,
+				Context:       remain.Inline,
+				ReqBodyLimit:  1024,
 				Proxies: producer.Proxies{
 					&producer.Proxy{Name: "default", RoundTrip: backend},
 				},
@@ -211,9 +211,9 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
 					&transport.Config{NoProxyFromEnv: true}, nil, logger)
 
 				ep := handler.NewEndpoint(&handler.EndpointOptions{
-					Error:        errors.DefaultJSON,
-					Context:      hcl.EmptyBody(),
-					ReqBodyLimit: 1024,
+					ErrorTemplate: errors.DefaultJSON,
+					Context:       hcl.EmptyBody(),
+					ReqBodyLimit:  1024,
 					Proxies: producer.Proxies{
 						&producer.Proxy{Name: "default", RoundTrip: backend},
 					},
@@ -333,10 +333,10 @@ func TestEndpoint_RoundTripContext_Null_Eval(t *testing.T) {
 			bufOpts := eval.MustBuffer(helper.NewInlineContext(tc.remain))
 
 			ep := handler.NewEndpoint(&handler.EndpointOptions{
-				BufferOpts:   bufOpts,
-				Context:      helper.NewInlineContext(tc.remain),
-				Error:        errors.DefaultJSON,
-				ReqBodyLimit: 1024,
+				BufferOpts:    bufOpts,
+				Context:       helper.NewInlineContext(tc.remain),
+				ErrorTemplate: errors.DefaultJSON,
+				ReqBodyLimit:  1024,
 				Proxies: producer.Proxies{
 					&producer.Proxy{Name: "default", RoundTrip: backend},
 				},
@@ -383,9 +383,8 @@ type mockProducerResult struct {
 	rt http.RoundTripper
 }
 
-func (m *mockProducerResult) Produce(_ context.Context, r *http.Request, results chan<- *producer.Result) {
+func (m *mockProducerResult) Produce(r *http.Request, results chan<- *producer.Result) {
 	if m == nil || m.rt == nil {
-		close(results)
 		return
 	}
 
@@ -395,7 +394,6 @@ func (m *mockProducerResult) Produce(_ context.Context, r *http.Request, results
 		Beresp:        res,
 		Err:           err,
 	}
-	close(results)
 }
 
 func (m *mockProducerResult) Len() int {
@@ -426,10 +424,10 @@ func TestEndpoint_ServeHTTP_FaultyDefaultResponse(t *testing.T) {
 	mockProducer := &mockProducerResult{rt}
 
 	ep := handler.NewEndpoint(&handler.EndpointOptions{
-		Context:  hcl.EmptyBody(),
-		Error:    errors.DefaultJSON,
-		Proxies:  &mockProducerResult{},
-		Requests: mockProducer,
+		Context:       hcl.EmptyBody(),
+		ErrorTemplate: errors.DefaultJSON,
+		Proxies:       &mockProducerResult{},
+		Requests:      mockProducer,
 	}, log.WithContext(context.Background()), nil)
 
 	ctx := context.Background()
@@ -478,10 +476,10 @@ func TestEndpoint_ServeHTTP_Cancel(t *testing.T) {
 	mockProducer := &mockProducerResult{rt}
 
 	ep := handler.NewEndpoint(&handler.EndpointOptions{
-		Context:  hcl.EmptyBody(),
-		Error:    errors.DefaultJSON,
-		Proxies:  &mockProducerResult{},
-		Requests: mockProducer,
+		Context:       hcl.EmptyBody(),
+		ErrorTemplate: errors.DefaultJSON,
+		Proxies:       &mockProducerResult{},
+		Requests:      mockProducer,
 	}, log.WithContext(ctx), nil)
 
 	req := httptest.NewRequest(http.MethodGet, "https://couper.io/", nil)
diff --git a/handler/producer/request_test.go b/handler/producer/request_test.go
new file mode 100644
index 00000000..a53883a0
--- /dev/null
+++ b/handler/producer/request_test.go
@@ -0,0 +1,123 @@
+package producer_test
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"strconv"
+	"testing"
+
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/zclconf/go-cty/cty"
+
+	hclbody "github.com/avenga/couper/config/body"
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/eval"
+	"github.com/avenga/couper/handler"
+	"github.com/avenga/couper/handler/producer"
+	"github.com/avenga/couper/handler/transport"
+	"github.com/avenga/couper/internal/test"
+)
+
+func Test_ProduceExpectedStatus(t *testing.T) {
+	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		s, err := strconv.Atoi(req.Header.Get("X-Status"))
+		if err != nil {
+			rw.WriteHeader(http.StatusBadRequest)
+			return
+		}
+		rw.WriteHeader(s)
+	}))
+	defer origin.Close()
+
+	logger, _ := test.NewLogger()
+	logEntry := logger.WithContext(context.Background())
+
+	backend := transport.NewBackend(hcl.EmptyBody(), &transport.Config{Origin: origin.URL}, nil, logEntry)
+
+	clientRequest, _ := http.NewRequest(http.MethodGet, "http://couper.local", nil)
+
+	toListVal := func(numbers ...int64) cty.Value {
+		var list []cty.Value
+		for _, n := range numbers {
+			list = append(list, cty.NumberIntVal(n))
+		}
+		return cty.ListVal(list)
+	}
+
+	tests := []struct {
+		name          string
+		attr          *hcl.Attribute
+		reflectStatus int // send via header, reflected by origin as http status-code
+		expectedErr   error
+	}{
+		{"/wo status", nil, http.StatusNoContent, nil},
+		{"/w status /w unexpected response", &hcl.Attribute{
+			Name: "expected_status",
+			Expr: &hclsyntax.LiteralValueExpr{Val: toListVal(200, 304)}},
+			http.StatusNotAcceptable,
+			errors.UnexpectedStatus,
+		},
+		{"/w status /w expected response", &hcl.Attribute{
+			Name: "expected_status",
+			Expr: &hclsyntax.LiteralValueExpr{Val: toListVal(200, 304)}},
+			http.StatusNotModified,
+			nil,
+		},
+	}
+
+	for _, tt := range tests {
+		content := &hcl.BodyContent{Attributes: map[string]*hcl.Attribute{
+			"url": {Name: "url", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal(origin.URL)}},
+			// Since request will not proxy our dynamic client-request header value, we will add a headers attr here.
+			// There is no validation, so this also applies to proxy (unused)
+			"set_request_headers": {Name: "set_request_headers", Expr: &hclsyntax.ObjectConsExpr{
+				Items: []hclsyntax.ObjectConsItem{
+					{
+						KeyExpr:   &hclsyntax.LiteralValueExpr{Val: cty.StringVal("X-Status")},
+						ValueExpr: &hclsyntax.LiteralValueExpr{Val: cty.NumberIntVal(int64(tt.reflectStatus))},
+					},
+				},
+			}},
+		}}
+		if tt.attr != nil {
+			content.Attributes[tt.attr.Name] = tt.attr
+		}
+
+		requests := producer.Requests{&producer.Request{
+			Backend: backend,
+			Context: hclbody.New(content),
+			Name:    "request",
+		}}
+
+		proxies := producer.Proxies{&producer.Proxy{
+			Name:      "proxy",
+			RoundTrip: handler.NewProxy(backend, hclbody.New(content), logEntry),
+		}}
+
+		testNames := []string{"request", "proxy"}
+
+		for i, rt := range []producer.Roundtrips{requests, proxies} {
+			t.Run(testNames[i]+"_"+tt.name, func(t *testing.T) {
+
+				ctx := eval.NewContext(nil, nil).WithClientRequest(clientRequest)
+
+				outreq := clientRequest.WithContext(ctx)
+				outreq.Header.Set("X-Status", strconv.Itoa(tt.reflectStatus))
+
+				resultCh := make(chan *producer.Result, 1)
+				rt.Produce(outreq, resultCh)
+				result := <-resultCh
+
+				if !errors.Equals(tt.expectedErr, result.Err) {
+					t.Fatalf("expected error: %v, got %v", tt.expectedErr, result.Err)
+				}
+
+				if result.Beresp == nil {
+					t.Fatal("expected a backend response")
+				}
+			})
+		}
+	}
+}
diff --git a/handler/transport/backend_test.go b/handler/transport/backend_test.go
index 98935d9a..c129c700 100644
--- a/handler/transport/backend_test.go
+++ b/handler/transport/backend_test.go
@@ -321,13 +321,15 @@ func TestBackend_director(t *testing.T) {
 			req := httptest.NewRequest(http.MethodGet, "https://example.com"+tt.path, nil)
 			*req = *req.WithContext(tt.ctx)
 
-			_, _ = backend.RoundTrip(req) // implicit director()
+			beresp, _ := backend.RoundTrip(req) // implicit director()
+			// outreq gets set on error cases
+			outreq := beresp.Request
 
 			attr, _ := hclContext.JustAttributes()
 			hostnameExp, ok := attr["hostname"]
 
-			if !ok && tt.expReq.Host != req.Host {
-				subT.Errorf("expected same host value, want: %q, got: %q", req.Host, tt.expReq.Host)
+			if !ok && tt.expReq.Host != outreq.Host {
+				subT.Errorf("expected same host value, want: %q, got: %q", outreq.Host, tt.expReq.Host)
 			} else if ok {
 				hostVal, _ := hostnameExp.Expr.Value(eval.NewContext(nil, nil).HCLContext())
 				hostname := seetie.ValueToString(hostVal)
@@ -336,8 +338,8 @@ func TestBackend_director(t *testing.T) {
 				}
 			}
 
-			if req.URL.Path != tt.expReq.URL.Path {
-				subT.Errorf("expected path: %q, got: %q", tt.expReq.URL.Path, req.URL.Path)
+			if outreq.URL.Path != tt.expReq.URL.Path {
+				subT.Errorf("expected path: %q, got: %q", tt.expReq.URL.Path, outreq.URL.Path)
 			}
 		})
 	}
diff --git a/internal/test/hcl.go b/internal/test/hcl.go
index 51ff4fb4..c98a817e 100644
--- a/internal/test/hcl.go
+++ b/internal/test/hcl.go
@@ -5,12 +5,12 @@ import (
 	"github.com/hashicorp/hcl/v2/hcltest"
 	"github.com/zclconf/go-cty/cty"
 
-	"github.com/avenga/couper/config/configload"
+	"github.com/avenga/couper/config/body"
 )
 
 func NewRemainContext(name, value string) hcl.Body {
 	expr := hcltest.MockExprLiteral(cty.StringVal(value))
-	return configload.MergeBodies([]hcl.Body{hcltest.MockBody(&hcl.BodyContent{Attributes: map[string]*hcl.Attribute{
+	return body.MergeBodies(hcltest.MockBody(&hcl.BodyContent{Attributes: map[string]*hcl.Attribute{
 		name: {Name: name, Expr: expr},
-	}})})
+	}}))
 }
diff --git a/internal/test/helper_proxy.go b/internal/test/helper_proxy.go
index 81e0505f..8a2c7efc 100644
--- a/internal/test/helper_proxy.go
+++ b/internal/test/helper_proxy.go
@@ -3,15 +3,14 @@ package test
 import (
 	"context"
 
-	"github.com/avenga/couper/handler/transport"
-
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclsimple"
 	"github.com/sirupsen/logrus/hooks/test"
 
-	"github.com/avenga/couper/config/configload"
+	"github.com/avenga/couper/config/body"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
+	"github.com/avenga/couper/handler/transport"
 )
 
 func (h *Helper) NewProxy(conf *transport.Config, backendContext, proxyContext hcl.Body) *handler.Proxy {
@@ -43,5 +42,5 @@ func (h *Helper) NewInlineContext(inlineHCL string) hcl.Body {
 
 	var remain hclBody
 	h.Must(hclsimple.Decode(h.tb.Name()+".hcl", []byte(inlineHCL), eval.NewContext(nil, nil).HCLContext(), &remain))
-	return configload.MergeBodies([]hcl.Body{remain.Inline})
+	return body.MergeBodies(remain.Inline)
 }
diff --git a/internal/test/test_backend.go b/internal/test/test_backend.go
index 7b6b5a51..60baaf90 100644
--- a/internal/test/test_backend.go
+++ b/internal/test/test_backend.go
@@ -37,6 +37,7 @@ func NewBackend() *Backend {
 	b.mux.HandleFunc("/error", func(w http.ResponseWriter, _ *http.Request) {
 		w.WriteHeader(http.StatusInternalServerError)
 	})
+	b.mux.HandleFunc("/reflect", reflect)
 
 	return b
 }
@@ -207,3 +208,10 @@ func oidc(rw http.ResponseWriter, req *http.Request) {
 			}`)
 	_, _ = rw.Write(body)
 }
+
+func reflect(rw http.ResponseWriter, req *http.Request) {
+	for k, v := range req.Header {
+		rw.Header()[k] = v
+	}
+	_, _ = io.Copy(rw, req.Body)
+}
diff --git a/oauth2/oidc/config_test.go b/oauth2/oidc/config_test.go
index e7c775d5..d3618b27 100644
--- a/oauth2/oidc/config_test.go
+++ b/oauth2/oidc/config_test.go
@@ -8,9 +8,10 @@ import (
 	"sync"
 	"testing"
 
+	"github.com/hashicorp/hcl/v2"
+
 	"github.com/avenga/couper/accesscontrol/jwk"
 	"github.com/avenga/couper/config"
-	"github.com/avenga/couper/config/configload"
 	"github.com/avenga/couper/handler/transport"
 	"github.com/avenga/couper/internal/test"
 	"github.com/avenga/couper/oauth2/oidc"
@@ -45,7 +46,7 @@ func TestConfig_Synced(t *testing.T) {
 	}))
 	defer origin.Close()
 
-	be := transport.NewBackend(configload.EmptyBody(), &transport.Config{}, nil, logger)
+	be := transport.NewBackend(hcl.EmptyBody(), &transport.Config{}, nil, logger)
 	o, err := oidc.NewConfig(&config.OIDC{ConfigurationURL: origin.URL + "/.well-known/openid-configuration"}, be)
 	helper.Must(err)
 
diff --git a/server/http_custom_logs_test.go b/server/http_custom_logs_test.go
index b81bc7d4..afe8e2e8 100644
--- a/server/http_custom_logs_test.go
+++ b/server/http_custom_logs_test.go
@@ -110,35 +110,56 @@ func TestCustomLogs_Local(t *testing.T) {
 	defer shutdown()
 
 	type testCase struct {
-		name string
-		path string
-		exp  logrus.Fields
+		name   string
+		path   string
+		header test.Header
+		exp    logrus.Fields
 	}
 
 	for _, tc := range []testCase{
-		{"basic-auth", "/secure", logrus.Fields{"error_handler": "GET"}},
-		{"jwt with error-handler", "/jwt", logrus.Fields{"jwt_error": "GET", "jwt_regular": "GET"}},
-		{"jwt with * error-handler", "/jwt-wildcard", logrus.Fields{"jwt_error_wildcard": "GET", "jwt_regular": "GET"}},
-		{"oauth2 error-handler", "/oauth2cb?pkcecv=qerbnr&error=qeuboub", logrus.Fields{"oauth2_error": "GET", "oauth2_regular": "GET"}},
-		{"oauth2 * error-handler", "/oauth2cb-wildcard?pkcecv=qerbnr&error=qeuboub", logrus.Fields{"oauth2_wildcard_error": "GET", "oauth2_regular": "GET"}},
-		{"saml with error-handler", "/saml/acs", logrus.Fields{"saml_error": "GET", "saml_regular": "GET"}},
-		{"saml with * error-handler", "/saml-wildcard/acs", logrus.Fields{"saml_wildcard_error": "GET", "saml_regular": "GET"}},
-		{"oidc with error-handler", "/oidc/cb", logrus.Fields{"oidc_error": "GET", "oidc_regular": "GET"}},
-		{"oidc with * error-handler", "/oidc-wildcard/cb", logrus.Fields{"oidc_wildcard_error": "GET", "oidc_regular": "GET"}},
-		{"file access", "/file.html", logrus.Fields{"files": "GET"}},
-		{"spa access", "/spa", logrus.Fields{"spa": "GET"}},
+		{"basic-auth", "/secure", nil, logrus.Fields{"error_handler": "GET"}},
+		{"jwt with error-handler", "/jwt", nil, logrus.Fields{"jwt_error": "GET", "jwt_regular": "GET"}},
+		{"jwt with * error-handler", "/jwt-wildcard", nil, logrus.Fields{"jwt_error_wildcard": "GET", "jwt_regular": "GET"}},
+		{"oauth2 error-handler", "/oauth2cb?pkcecv=qerbnr&error=qeuboub", nil, logrus.Fields{"oauth2_error": "GET", "oauth2_regular": "GET"}},
+		{"oauth2 * error-handler", "/oauth2cb-wildcard?pkcecv=qerbnr&error=qeuboub", nil, logrus.Fields{"oauth2_wildcard_error": "GET", "oauth2_regular": "GET"}},
+		{"saml with error-handler", "/saml/acs", nil, logrus.Fields{"saml_error": "GET", "saml_regular": "GET"}},
+		{"saml with * error-handler", "/saml-wildcard/acs", nil, logrus.Fields{"saml_wildcard_error": "GET", "saml_regular": "GET"}},
+		{"oidc with error-handler", "/oidc/cb", nil, logrus.Fields{"oidc_error": "GET", "oidc_regular": "GET"}},
+		{"oidc with * error-handler", "/oidc-wildcard/cb", nil, logrus.Fields{"oidc_wildcard_error": "GET", "oidc_regular": "GET"}},
+		{"file access", "/file.html", nil, logrus.Fields{"files": "GET"}},
+		{"spa access", "/spa", nil, logrus.Fields{"spa": "GET"}},
+		{"endpoint with error-handler", "/error-handler/endpoint",
+			test.Header{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Qf0lkeZKZ3NJrYm3VdgiQiQ6QTrjCvISshD_q9F8GAM"},
+			logrus.Fields{"error_handler": "GET", "jwt_regular": "GET"}},
+		{"endpoint standard", "/standard", nil, logrus.Fields{
+			"server": "couper test-backend",
+			"item-1": "item1",
+			"item-2": "item1",
+		}},
+		{"endpoint sequence", "/sequence", nil, logrus.Fields{
+			"server":     "couper test-backend",
+			"seq-item-1": "item1",
+			"seq-item-2": "item1",
+		}},
 	} {
 		t.Run(tc.name, func(st *testing.T) {
+			hook.Reset()
+
 			helper := test.New(st)
 			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
 			helper.Must(err)
 
-			hook.Reset()
-			_, err = client.Do(req)
+			for k, v := range tc.header {
+				req.Header.Set(k, v)
+			}
+
+			res, err := client.Do(req)
 			helper.Must(err)
 
+			helper.Must(res.Body.Close())
+
 			// Wait for logs
-			time.Sleep(200 * time.Millisecond)
+			time.Sleep(time.Second / 5)
 
 			// Access log
 			entries := hook.AllEntries()
@@ -147,7 +168,18 @@ func TestCustomLogs_Local(t *testing.T) {
 				return
 			}
 
-			got, ok := entries[0].Data["custom"].(logrus.Fields)
+			var accessLogEntry *logrus.Entry
+			for _, e := range entries {
+				if e.Data["type"] == "couper_access" {
+					accessLogEntry = e
+					break
+				}
+			}
+			if accessLogEntry == nil {
+				st.Fatal("Expected access log entry")
+			}
+
+			got, ok := accessLogEntry.Data["custom"].(logrus.Fields)
 			if !ok {
 				st.Fatal("expected custom log field, got none")
 			}
diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go
index 5229afdc..2ca2634c 100644
--- a/server/http_endpoints_test.go
+++ b/server/http_endpoints_test.go
@@ -5,8 +5,10 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
 	"net/http"
+	"net/http/httptest"
 	"path"
 	"reflect"
 	"strconv"
@@ -14,7 +16,10 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
+
 	"github.com/avenga/couper/internal/test"
+	"github.com/avenga/couper/logging"
 )
 
 const testdataPath = "testdata/endpoints"
@@ -437,3 +442,240 @@ func TestHTTPServer_NoGzipForSmallContent(t *testing.T) {
 		})
 	}
 }
+
+func TestEndpointSequence(t *testing.T) {
+	client := test.NewHTTPClient()
+	helper := test.New(t)
+
+	shutdown, _ := newCouper(path.Join(testdataPath, "11_couper.hcl"), helper)
+	defer shutdown()
+
+	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		rw.Header().Set("Y-Value", "my-value")
+		if req.Header.Get("Accept") == "application/json" {
+			rw.Header().Set("Content-Type", "application/json")
+			_, _ = fmt.Fprintf(rw, `{"value":"%s"}`, req.Header.Get("X-Value"))
+		} else {
+			rw.WriteHeader(http.StatusNoContent)
+		}
+	}))
+	defer origin.Close()
+
+	type testcase struct {
+		name           string
+		path           string
+		expectedHeader test.Header
+		expectedBody   string
+	}
+
+	for _, tc := range []testcase{
+		{"simple request sequence", "/simple", test.Header{"x": "my-value"}, `{"value":"my-value"}`},
+		{"simple request/proxy sequence", "/simple-proxy", test.Header{"x": "my-value", "y": `{"value":"my-proxy-value"}`}, ""},
+		{"simple proxy/request sequence", "/simple-proxy-named", test.Header{"x": "my-value"}, ""},
+		{"complex request/proxy sequence", "/complex-proxy", test.Header{"x": "my-value"}, ""},
+		{"complex request/proxy sequences", "/parallel-complex-proxy", test.Header{"x": "my-value", "y": "my-value", "z": "my-value"}, ""},
+	} {
+		t.Run(tc.name, func(st *testing.T) {
+			h := test.New(st)
+
+			req, err := http.NewRequest(http.MethodGet, "http://domain.local:8080"+tc.path, nil)
+			h.Must(err)
+
+			req.Header.Set("Origin", origin.URL)
+
+			res, err := client.Do(req)
+			h.Must(err)
+
+			if res.StatusCode != http.StatusOK {
+				st.Fatal("expected status ok")
+			}
+
+			for k, v := range tc.expectedHeader {
+				if hv := res.Header.Get(k); hv != v {
+					st.Errorf("%q: want %q, got %q", k, v, hv)
+					break
+				}
+			}
+
+			if tc.expectedBody != "" {
+				result, err := io.ReadAll(res.Body)
+				h.Must(err)
+
+				if tc.expectedBody != string(result) {
+					st.Errorf("unexpected body:\n%s", cmp.Diff(tc.expectedBody, string(result)))
+				}
+			}
+
+		})
+	}
+
+}
+
+func TestEndpointSequenceClientCancel(t *testing.T) {
+	client := test.NewHTTPClient()
+	helper := test.New(t)
+
+	shutdown, hook := newCouper(path.Join(testdataPath, "12_couper.hcl"), helper)
+	defer shutdown()
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		cancel()
+		time.Sleep(time.Second / 2)
+		rw.WriteHeader(http.StatusNoContent)
+	}))
+	defer origin.Close()
+
+	hook.Reset()
+
+	req, err := http.NewRequest(http.MethodGet, "http://domain.local:8080/", nil)
+	helper.Must(err)
+
+	req.Header.Set("Origin", origin.URL)
+
+	_, err = client.Do(req.WithContext(ctx))
+	if err != nil && errors.Unwrap(err) != context.Canceled {
+		helper.Must(err)
+	}
+
+	time.Sleep(time.Second / 2)
+
+	logs := hook.AllEntries()
+
+	var ctxCanceledSeen, statusOKseen bool
+	for _, entry := range logs {
+		if entry.Data["type"] != "couper_backend" {
+			continue
+		}
+
+		path, _ := entry.Data["request"].(logging.Fields)["path"]
+
+		if strings.Contains(entry.Message, context.Canceled.Error()) {
+			ctxCanceledSeen = true
+			if path != "/" {
+				t.Errorf("expected '/' to fail")
+			}
+		}
+
+		if entry.Message == "" && entry.Data["status"] == 200 {
+			statusOKseen = true
+			if path != "/reflect" {
+				t.Errorf("expected '/reflect' to be ok")
+			}
+		}
+	}
+
+	if !ctxCanceledSeen || !statusOKseen {
+		t.Errorf("Expected one sucessful and one failed backend request")
+	}
+
+}
+
+func TestEndpointSequenceBackendTimeout(t *testing.T) {
+	client := test.NewHTTPClient()
+	helper := test.New(t)
+
+	shutdown, hook := newCouper(path.Join(testdataPath, "13_couper.hcl"), helper)
+	defer shutdown()
+
+	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		time.Sleep(time.Second)
+		rw.WriteHeader(http.StatusNoContent)
+	}))
+	defer origin.Close()
+
+	hook.Reset()
+
+	req, err := http.NewRequest(http.MethodGet, "http://domain.local:8080/", nil)
+	helper.Must(err)
+
+	req.Header.Set("Origin", origin.URL)
+
+	res, err := client.Do(req)
+	if err != nil {
+		helper.Must(err)
+	}
+
+	if res.StatusCode != http.StatusBadGateway {
+		t.Fatalf("Expected status 502, got: %d", res.StatusCode)
+	}
+
+	time.Sleep(time.Second / 4)
+
+	logs := hook.AllEntries()
+
+	var ctxDeadlineSeen, statusOKseen bool
+	for _, entry := range logs {
+		if entry.Data["type"] != "couper_backend" {
+			continue
+		}
+
+		path, _ := entry.Data["request"].(logging.Fields)["path"]
+
+		if entry.Message == "backend timeout error: deadline exceeded" {
+			ctxDeadlineSeen = true
+			if path != "/" {
+				t.Errorf("expected '/' to fail")
+			}
+		}
+
+		if entry.Message == "" && entry.Data["status"] == 200 {
+			statusOKseen = true
+			if path != "/reflect" {
+				t.Errorf("expected '/reflect' to be ok")
+			}
+		}
+	}
+
+	if !ctxDeadlineSeen || !statusOKseen {
+		t.Errorf("Expected one sucessful and one failed backend request")
+	}
+
+}
+
+func TestEndpointErrorHandler(t *testing.T) {
+	client := test.NewHTTPClient()
+	helper := test.New(t)
+
+	shutdown, hook := newCouper(path.Join(testdataPath, "14_couper.hcl"), helper)
+	defer shutdown()
+	defer func() {
+		for _, e := range hook.AllEntries() {
+			t.Logf("%#v", e.Data)
+		}
+	}()
+
+	type testcase struct {
+		name           string
+		path           string
+		expectedHeader test.Header
+		expectedStatus int
+	}
+
+	for _, tc := range []testcase{
+		{"error_handler not triggered", "/ok", test.Header{"x": "application/json"}, http.StatusOK},
+		{"error_handler triggered with beresp body", "/not-ok", test.Header{"x": "200", "y": "item1"}, http.StatusTeapot},
+	} {
+		t.Run(tc.name, func(st *testing.T) {
+			h := test.New(st)
+
+			req, err := http.NewRequest(http.MethodGet, "http://domain.local:8080"+tc.path, nil)
+			h.Must(err)
+
+			res, err := client.Do(req)
+			h.Must(err)
+
+			if res.StatusCode != tc.expectedStatus {
+				st.Fatalf("want: %d, got: %d", tc.expectedStatus, res.StatusCode)
+			}
+
+			for k, v := range tc.expectedHeader {
+				if hv := res.Header.Get(k); hv != v {
+					st.Errorf("%q: want %q, got %q", k, v, hv)
+					break
+				}
+			}
+		})
+	}
+}
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index e0def554..930ecaed 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -164,19 +164,7 @@ func newCouperWithConfig(couperConfig *config.Couper, helper *test.Helper) (func
 }
 
 func newClient() *http.Client {
-	dialer := &net.Dialer{}
-	return &http.Client{
-		Transport: &http.Transport{
-			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
-				_, port, _ := net.SplitHostPort(addr)
-				if port != "" {
-					return dialer.DialContext(ctx, "tcp4", "127.0.0.1:"+port)
-				}
-				return dialer.DialContext(ctx, "tcp4", "127.0.0.1")
-			},
-			DisableCompression: true,
-		},
-	}
+	return test.NewHTTPClient()
 }
 
 func cleanup(shutdown func(), helper *test.Helper) {
@@ -3961,7 +3949,7 @@ func TestCORS_Configuration(t *testing.T) {
 }
 
 func TestLog_Level(t *testing.T) {
-	shutdown, hook := newCouper("testdata/integration/logging/01_couper.hcl", test.New(t))
+	shutdown, hook := newCouper("testdata/integration/logs/03_couper.hcl", test.New(t))
 	defer shutdown()
 
 	client := newClient()
diff --git a/server/mux_test.go b/server/mux_test.go
index 8421f2ff..56c7e9a2 100644
--- a/server/mux_test.go
+++ b/server/mux_test.go
@@ -69,7 +69,7 @@ func TestMux_FindHandler_PathParamContext(t *testing.T) {
 		{" /w path param and expWildcard", newReq("/project/foo/bar/ha"), noContent, request.PathParameter{
 			"project": "foo",
 		}, "bar/ha"},
-		{" /w non existing path", newReq("/foo/{bar}/123"), errors.DefaultJSON.ServeError(errors.RouteNotFound), nil, ""},
+		{" /w non existing path", newReq("/foo/{bar}/123"), errors.DefaultJSON.WithError(errors.RouteNotFound), nil, ""},
 		{" files", newReq("/htdocs/test.html"), http.NotFoundHandler(), request.PathParameter{
 			"section": "htdocs",
 			"project": "test.html",
diff --git a/server/testdata/endpoints/11_couper.hcl b/server/testdata/endpoints/11_couper.hcl
new file mode 100644
index 00000000..27a39ec7
--- /dev/null
+++ b/server/testdata/endpoints/11_couper.hcl
@@ -0,0 +1,113 @@
+server { # sequences
+  endpoint "/simple" {
+    request "resolve" {
+      url = "${request.headers.origin}/"
+      headers = {
+        Accept = "application/json"
+        X-Value = "my-value"
+      }
+    }
+
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        x = backend_responses.resolve.headers.y-value
+      }
+      json_body = backend_responses.resolve.json_body
+    }
+  }
+
+  endpoint "/simple-proxy" {
+    request "resolve" {
+      url = "${request.headers.origin}/"
+      headers = {
+        Accept = "application/json"
+        X-Value = "my-proxy-value"
+      }
+    }
+
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      set_request_headers = {
+        x = backend_responses.resolve.headers.y-value
+        y = backend_responses.resolve.body
+      }
+    }
+  }
+
+  endpoint "/simple-proxy-named" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        x = backend_responses.resolve.headers.y-value
+      }
+    }
+
+    proxy "resolve" {
+      url = "${request.headers.origin}/"
+    }
+  }
+
+  endpoint "/complex-proxy" {
+    request "resolve" { # use the reflected origin header to obtain the y-value
+      url = "${backend_responses.resolve_first.headers.origin}/"
+    }
+
+    request "resolve_first" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        origin = request.headers.origin
+      }
+    }
+
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      set_request_headers = {
+        x = backend_responses.resolve.headers.y-value
+      }
+    }
+  }
+
+  endpoint "/parallel-complex-proxy" {
+    request "standalone" { # parallel with both sequences
+      url = "${backend_responses.resolve_first.headers.origin}/"
+    }
+
+    request "resolve" { # use the reflected origin header to obtain the y-value
+      url = "${backend_responses.resolve_first.headers.origin}/"
+    }
+
+    request "resolve_first" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        origin = request.headers.origin
+      }
+    }
+
+    request "resolve_gamma" { # use the reflected origin header to obtain the y-value
+      url = "${backend_responses.resolve_gamma_first.headers.origin}/"
+    }
+
+    request "resolve_gamma_first" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        origin = request.headers.origin
+      }
+    }
+
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      set_request_headers = {
+        x = backend_responses.resolve.headers.y-value
+      }
+    }
+
+    response {
+      headers = {
+        x = backend_responses.default.headers.x
+        y = backend_responses.resolve_gamma.headers.y-value
+        z = backend_responses.standalone.headers.y-value
+      }
+    }
+  }
+}
diff --git a/server/testdata/endpoints/12_couper.hcl b/server/testdata/endpoints/12_couper.hcl
new file mode 100644
index 00000000..516e0375
--- /dev/null
+++ b/server/testdata/endpoints/12_couper.hcl
@@ -0,0 +1,21 @@
+server { # sequences
+  endpoint "/" {
+    request "resolve" { # use the reflected origin header to obtain the y-value
+      url = "${backend_responses.resolve_first.headers.origin}/" # this backend will trigger a client cancel
+    }
+
+    request "resolve_first" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        origin = request.headers.origin
+      }
+    }
+
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      set_request_headers = {
+        x = backend_responses.resolve.headers.y-value
+      }
+    }
+  }
+}
diff --git a/server/testdata/endpoints/13_couper.hcl b/server/testdata/endpoints/13_couper.hcl
new file mode 100644
index 00000000..60fc55f8
--- /dev/null
+++ b/server/testdata/endpoints/13_couper.hcl
@@ -0,0 +1,26 @@
+server { # sequences
+  endpoint "/" {
+    request "resolve" { # use the reflected origin header to obtain the y-value
+      backend {
+        origin = "${backend_responses.resolve_first.headers.origin}"
+        hostname = "test.local"
+        path = "/"
+        timeout = "1s"
+      }
+    }
+
+    request "resolve_first" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        origin = request.headers.origin
+      }
+    }
+
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      set_request_headers = {
+        x = backend_responses.resolve.headers.y-value
+      }
+    }
+  }
+}
diff --git a/server/testdata/endpoints/14_couper.hcl b/server/testdata/endpoints/14_couper.hcl
new file mode 100644
index 00000000..2837bd30
--- /dev/null
+++ b/server/testdata/endpoints/14_couper.hcl
@@ -0,0 +1,45 @@
+server { # error_handler
+  endpoint "/ok" {
+    request "resolve" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+
+      expected_status = [200, 204]
+    }
+
+    custom_log_fields = {
+      beresp_res = backend_responses.resolve
+      beresp_def = backend_responses.default
+    }
+
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      set_request_headers = {
+        x = backend_responses.resolve.headers.content-type
+      }
+    }
+
+    error_handler "unexpected_status" {
+      response {
+        status = 418
+      }
+    }
+  }
+
+  endpoint "/not-ok" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+
+      expected_status = [418]
+    }
+
+    error_handler "unexpected_status" {
+      response {
+        headers = {
+          x = backend_responses.default.status
+          y = backend_responses.default.json_body.Json.list[0]
+        }
+        status = 418
+      }
+    }
+  }
+}
diff --git a/server/testdata/integration/logs/01_couper.hcl b/server/testdata/integration/logs/01_couper.hcl
index 7e56f3ef..2f44f1fa 100644
--- a/server/testdata/integration/logs/01_couper.hcl
+++ b/server/testdata/integration/logs/01_couper.hcl
@@ -143,7 +143,56 @@ server "logs" {
       }
     }
 
+    endpoint "/error-handler/endpoint" {
+      access_control = ["JWT"]
+
+      beta_scope = "required"
+
+      response {
+        status = 204
+      }
+
+      error_handler "beta_insufficient_scope" {
+        custom_log_fields = {
+          error_handler = request.method
+        }
+      }
+    }
   }
+
+  endpoint "/standard" {
+    request "resolve" {
+      backend = "BE"
+    }
+
+    proxy {
+      backend = "BE"
+    }
+
+    custom_log_fields = {
+      item-1 = backend_responses.resolve.json_body.Json.list[0]
+      item-2 = backend_responses.default.json_body.Json.list[0]
+    }
+  }
+
+  endpoint "/sequence" {
+    request "resolve" {
+      backend = "BE"
+    }
+
+    proxy {
+      backend = "BE"
+      set_request_headers = {
+        x-status = backend_responses.resolve.status
+      }
+    }
+
+    custom_log_fields = {
+      seq-item-1 = backend_responses.resolve.json_body.Json.list[0]
+      seq-item-2 = backend_responses.default.json_body.Json.list[0]
+    }
+  }
+
 }
 
 definitions {