Repository: avenga/couper
Pull request #131 (2021-03-09 16:40:04)

Refactor: hcl evaluation context and related settings as own context obj
Attached to request which fixes the variable usage of non manipulated req and beresp* attributes

* at least at backend level


Patch:
diff --git a/command/run.go b/command/run.go
index 335837b6..09e8257e 100644
--- a/command/run.go
+++ b/command/run.go
@@ -47,7 +47,7 @@ func (r Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry) e
 		return err
 	}
 
-	serverList, listenCmdShutdown := server.NewServerList(r.context, logEntry.Logger, config.Settings, &timings, srvMux)
+	serverList, listenCmdShutdown := server.NewServerList(r.context, config.Context, logEntry.Logger, config.Settings, &timings, srvMux)
 	for _, srv := range serverList {
 		srv.Listen()
 	}
diff --git a/config/configload/load.go b/config/configload/load.go
index 1b798b1e..2300d4c8 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -58,17 +58,17 @@ func LoadBytes(src []byte, filename string) (*config.Couper, error) {
 var envContext *hcl.EvalContext
 
 func LoadConfig(body hcl.Body, src []byte) (*config.Couper, error) {
-	envContext = eval.NewENVContext(src)
-
 	defaults := config.DefaultSettings
 
 	couperConfig := &config.Couper{
 		Bytes:       src,
-		Context:     envContext,
+		Context:     eval.NewContext(src),
 		Definitions: &config.Definitions{},
 		Settings:    &defaults,
 	}
 
+	envContext = couperConfig.Context.HCLContext()
+
 	schema, _ := gohcl.ImpliedBodySchema(couperConfig)
 	content, diags := body.Content(schema)
 	if content == nil {
@@ -103,11 +103,11 @@ func LoadConfig(body hcl.Body, src []byte) (*config.Couper, error) {
 				}
 			}
 
-			if diags = gohcl.DecodeBody(leftOver, couperConfig.Context, couperConfig.Definitions); diags.HasErrors() {
+			if diags = gohcl.DecodeBody(leftOver, envContext, couperConfig.Definitions); diags.HasErrors() {
 				return nil, diags
 			}
 		case settings:
-			if diags = gohcl.DecodeBody(outerBlock.Body, couperConfig.Context, couperConfig.Settings); diags.HasErrors() {
+			if diags = gohcl.DecodeBody(outerBlock.Body, envContext, couperConfig.Settings); diags.HasErrors() {
 				return nil, diags
 			}
 		}
@@ -116,7 +116,7 @@ func LoadConfig(body hcl.Body, src []byte) (*config.Couper, error) {
 	// Read per server block and merge backend settings which results in a final server configuration.
 	for _, serverBlock := range content.Blocks.OfType(server) {
 		serverConfig := &config.Server{}
-		if diags = gohcl.DecodeBody(serverBlock.Body, couperConfig.Context, serverConfig); diags.HasErrors() {
+		if diags = gohcl.DecodeBody(serverBlock.Body, envContext, serverConfig); diags.HasErrors() {
 			return nil, diags
 		}
 
diff --git a/config/couper.go b/config/couper.go
index 19f0bfcc..895a3c0a 100644
--- a/config/couper.go
+++ b/config/couper.go
@@ -1,7 +1,7 @@
 package config
 
 import (
-	"github.com/hashicorp/hcl/v2"
+	"github.com/avenga/couper/eval"
 )
 
 // DefaultFilename defines the default filename for a couper config file.
@@ -10,7 +10,7 @@ const DefaultFilename = "couper.hcl"
 // Couper represents the <Couper> config object.
 type Couper struct {
 	Bytes       []byte
-	Context     *hcl.EvalContext
+	Context     *eval.Context
 	Definitions *Definitions `hcl:"definitions,block"`
 	Servers     Servers      `hcl:"server,block"`
 	Settings    *Settings    `hcl:"settings,block"`
diff --git a/config/runtime/server.go b/config/runtime/server.go
index 61f21dac..6280e12a 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -14,6 +14,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/docker/go-units"
 	"github.com/getkin/kin-openapi/pathpattern"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
@@ -69,7 +70,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfi
 	noopReq := httptest.NewRequest(http.MethodGet, "https://couper.io", nil)
 	noopResp := httptest.NewRecorder().Result()
 	noopResp.Request = noopReq
-	confCtx := eval.NewHTTPContext(conf.Context, 0, noopReq, noopResp)
+	confCtx := conf.Context.WithClientRequest(noopReq).WithBeresps(noopResp).HCLContext()
 
 	validPortMap, hostsMap, err := validatePortHosts(conf, defaultPort)
 	if err != nil {
@@ -185,7 +186,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfi
 				if berr != nil {
 					return nil, berr
 				}
-				proxyHandler := handler.NewProxy(backend, proxyConf.HCLBody(), confCtx)
+				proxyHandler := handler.NewProxy(backend, proxyConf.HCLBody())
 				p := &producer.Proxy{
 					Name:      proxyConf.Name,
 					RoundTrip: proxyHandler,
@@ -231,7 +232,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfi
 				kind = api
 			}
 
-			bodyLimit, err := handler.ParseBodyLimit(endpointConf.RequestBodyLimit)
+			bodyLimit, err := parseBodyLimit(endpointConf.RequestBodyLimit)
 			if err != nil {
 				r := endpointConf.Remain.MissingItemRange()
 				return nil, hcl.Diagnostics{&hcl.Diagnostic{
@@ -256,7 +257,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfi
 				ReqBufferOpts:  bufferOpts,
 				ServerOpts:     serverOptions,
 			}
-			epHandler := handler.NewEndpoint(epOpts, confCtx, log, proxies, requests, response)
+			epHandler := handler.NewEndpoint(epOpts, log, proxies, requests, response)
 			setACHandlerFn(epHandler)
 
 			err = setRoutesFromHosts(serverConfiguration, serverOptions.ServerErrTpl, defaultPort, srvConf.Hosts, pattern, endpointHandlers[endpointConf], kind)
@@ -303,7 +304,7 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry
 		return nil, err
 	}
 
-	backend := transport.NewBackend(evalCtx, backendCtx, tc, log, openAPIopts)
+	backend := transport.NewBackend(backendCtx, tc, log, openAPIopts)
 	return backend, nil
 }
 
@@ -483,3 +484,12 @@ func parseDuration(src string, target *time.Duration) error {
 	*target = d
 	return nil
 }
+
+func parseBodyLimit(limit string) (int64, error) {
+	const defaultReqBodyLimit = "64MiB"
+	requestBodyLimit := defaultReqBodyLimit
+	if limit != "" {
+		requestBodyLimit = limit
+	}
+	return units.FromHumanSize(requestBodyLimit)
+}
diff --git a/eval/context.go b/eval/context.go
index dc10b3b6..5d9f5ff5 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -11,6 +11,7 @@ import (
 	"net/url"
 	"os"
 	"strconv"
+	"time"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
@@ -24,10 +25,9 @@ import (
 	"github.com/avenga/couper/internal/seetie"
 )
 
-type Roundtrip interface {
-	Context() context.Context
-	Cookies() []*http.Cookie
-}
+const ContextType = "evalContextType"
+
+var _ context.Context = &Context{}
 
 type ContextMap map[string]cty.Value
 
@@ -38,40 +38,67 @@ func (m ContextMap) Merge(other ContextMap) ContextMap {
 	return m
 }
 
-func NewENVContext(src []byte) *hcl.EvalContext {
+type Context struct {
+	bufferOption BufferOption
+	eval         *hcl.EvalContext
+	inner        context.Context
+}
+
+func NewContext(src []byte) *Context {
 	envKeys := decodeEnvironmentRefs(src)
 	variables := make(map[string]cty.Value)
 	variables[Environment] = newCtyEnvMap(envKeys)
 
-	return &hcl.EvalContext{
-		Variables: variables,
-		Functions: newFunctionsMap(),
+	return &Context{
+		bufferOption: BufferRequest | BufferResponse, // TODO: eval per endpoint body route
+		eval: &hcl.EvalContext{
+			Variables: variables,
+			Functions: newFunctionsMap(),
+		}}
+}
+
+func (c *Context) Deadline() (deadline time.Time, ok bool) {
+	return c.inner.Deadline()
+}
+
+func (c *Context) Done() <-chan struct{} {
+	return c.inner.Done()
+}
+
+func (c *Context) Err() error {
+	return c.inner.Err()
+}
+
+func (c *Context) Value(key interface{}) interface{} {
+	if key == ContextType {
+		return c
 	}
+	return c.inner.Value(key)
 }
 
-func NewHTTPContext(baseCtx *hcl.EvalContext, bufOpt BufferOption, req *http.Request, beresps ...*http.Response) *hcl.EvalContext {
-	if req == nil {
-		return baseCtx
+func (c *Context) WithClientRequest(req *http.Request) *Context {
+	ctx := &Context{
+		bufferOption: c.bufferOption,
+		eval:         cloneContext(c.eval),
 	}
-	evalCtx := cloneContext(baseCtx)
-	httpCtx := req.Context()
+	ctx.inner = context.WithValue(req.Context(), ContextType, ctx)
 
-	reqCtxMap := ContextMap{}
-	if endpoint, ok := httpCtx.Value(request.Endpoint).(string); ok {
-		reqCtxMap[Endpoint] = cty.StringVal(endpoint)
+	ctxMap := ContextMap{}
+	if endpoint, ok := ctx.inner.Value(request.Endpoint).(string); ok {
+		ctxMap[Endpoint] = cty.StringVal(endpoint)
 	}
 
 	var id string
-	if uid, ok := httpCtx.Value(request.UID).(string); ok {
+	if uid, ok := ctx.inner.Value(request.UID).(string); ok {
 		id = uid
 	}
 
 	var pathParams request.PathParameter
-	if params, ok := req.Context().Value(request.PathParams).(request.PathParameter); ok {
+	if params, ok := ctx.inner.Value(request.PathParams).(request.PathParameter); ok {
 		pathParams = params
 	}
 
-	evalCtx.Variables[ClientRequest] = cty.ObjectVal(reqCtxMap.Merge(ContextMap{
+	ctx.eval.Variables[ClientRequest] = cty.ObjectVal(ctxMap.Merge(ContextMap{
 		ID:        cty.StringVal(id),
 		JsonBody:  seetie.MapToValue(parseReqJSON(req)),
 		Method:    cty.StringVal(req.Method),
@@ -80,11 +107,17 @@ func NewHTTPContext(baseCtx *hcl.EvalContext, bufOpt BufferOption, req *http.Req
 		Post:      seetie.ValuesMapToValue(parseForm(req).PostForm),
 		Query:     seetie.ValuesMapToValue(req.URL.Query()),
 		URL:       cty.StringVal(newRawURL(req.URL).String()),
-	}.Merge(newVariable(httpCtx, req.Cookies(), req.Header))))
+	}.Merge(newVariable(ctx.inner, req.Cookies(), req.Header))))
+
+	return ctx
+}
 
-	if len(beresps) == 0 {
-		return evalCtx
+func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
+	ctx := &Context{
+		bufferOption: c.bufferOption,
+		eval:         cloneContext(c.eval),
 	}
+	ctx.inner = context.WithValue(c.inner, ContextType, ctx)
 
 	resps := make(ContextMap, 0)
 	bereqs := make(ContextMap, 0)
@@ -100,47 +133,35 @@ func NewHTTPContext(baseCtx *hcl.EvalContext, bufOpt BufferOption, req *http.Req
 			Post:   seetie.ValuesMapToValue(parseForm(bereq).PostForm),
 			Query:  seetie.ValuesMapToValue(bereq.URL.Query()),
 			URL:    cty.StringVal(newRawURL(bereq.URL).String()),
-		}.Merge(newVariable(httpCtx, bereq.Cookies(), bereq.Header)))
+		}.Merge(newVariable(ctx.inner, bereq.Cookies(), bereq.Header)))
 
 		var jsonBody map[string]interface{}
-		if (bufOpt & BufferResponse) == BufferResponse {
+		if (ctx.bufferOption & BufferResponse) == BufferResponse {
 			jsonBody = parseRespJSON(beresp)
 		}
 		resps[name] = cty.ObjectVal(ContextMap{
 			HttpStatus: cty.StringVal(strconv.Itoa(beresp.StatusCode)),
 			JsonBody:   seetie.MapToValue(jsonBody),
-		}.Merge(newVariable(httpCtx, beresp.Cookies(), beresp.Header)))
+		}.Merge(newVariable(ctx.inner, beresp.Cookies(), beresp.Header)))
 	}
 
 	if val, ok := bereqs[BackendDefault]; ok {
-		evalCtx.Variables[BackendRequest] = val
+		ctx.eval.Variables[BackendRequest] = val
 	}
 	if val, ok := resps[BackendDefault]; ok {
-		evalCtx.Variables[BackendResponse] = val
+		ctx.eval.Variables[BackendResponse] = val
 	}
-	evalCtx.Variables[BackendRequests] = cty.ObjectVal(bereqs)
-	evalCtx.Variables[BackendResponses] = cty.ObjectVal(resps)
+	ctx.eval.Variables[BackendRequests] = cty.ObjectVal(bereqs)
+	ctx.eval.Variables[BackendResponses] = cty.ObjectVal(resps)
 
-	return evalCtx
+	return ctx
 }
 
-const defaultMaxMemory = 32 << 20 // 32 MB
-
-type ReadCloser struct {
-	io.Reader
-	closer io.Closer
+func (c Context) HCLContext() *hcl.EvalContext {
+	return c.eval
 }
 
-func NewReadCloser(r io.Reader, c io.Closer) *ReadCloser {
-	return &ReadCloser{Reader: r, closer: c}
-}
-
-func (rc ReadCloser) Close() error {
-	if rc.closer == nil {
-		return nil
-	}
-	return rc.closer.Close()
-}
+const defaultMaxMemory = 32 << 20 // 32 MB
 
 // parseForm populates the request PostForm field.
 // As Proxy we should not consume the request body.
@@ -244,7 +265,7 @@ func newVariable(ctx context.Context, cookies []*http.Cookie, headers http.Heade
 	}
 
 	return map[string]cty.Value{
-		Context: ctxAcMapValue,
+		CTX:     ctxAcMapValue,
 		Cookies: seetie.CookiesToMapValue(cookies),
 		Headers: seetie.HeaderToMapValue(headers),
 	}
diff --git a/eval/http.go b/eval/http.go
index c843c6ae..e3de4e39 100644
--- a/eval/http.go
+++ b/eval/http.go
@@ -1,6 +1,9 @@
 package eval
 
 import (
+	"bytes"
+	"context"
+	"io"
 	"net/http"
 	"strconv"
 	"strings"
@@ -10,6 +13,7 @@ import (
 
 	"github.com/avenga/couper/config/meta"
 	"github.com/avenga/couper/config/request"
+	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/internal/seetie"
 	"github.com/avenga/couper/utils"
 )
@@ -30,14 +34,49 @@ const (
 	attrDelResHeaders = "remove_response_headers"
 )
 
-func ApplyRequestContext(ctx *hcl.EvalContext, body hcl.Body, req *http.Request) error {
+// SetGetBody determines if we have to buffer a request body for further processing.
+// First of all the user has a related reference within a related options context declaration.
+// Additionally the request body is nil or a NoBody type and the http method has no body restrictions like 'TRACE'.
+func SetGetBody(req *http.Request, bodyLimit int64) error {
+	if req.Method == http.MethodTrace {
+		return nil
+	}
+
+	// TODO: handle buffer options based on overall body context and reference
+	//if (e.opts.ReqBufferOpts & eval.BufferRequest) != eval.BufferRequest {
+	//	return nil
+	//}
+
+	if req.Body != nil && req.Body != http.NoBody && req.GetBody == nil {
+		buf := &bytes.Buffer{}
+		lr := io.LimitReader(req.Body, bodyLimit+1)
+		n, err := buf.ReadFrom(lr)
+		if err != nil {
+			return err
+		}
+
+		if n > bodyLimit {
+			return errors.EndpointReqBodySizeExceeded
+		}
+
+		bodyBytes := buf.Bytes()
+		req.GetBody = func() (io.ReadCloser, error) {
+			return NewReadCloser(bytes.NewBuffer(bodyBytes), req.Body), nil
+		}
+	}
+
+	return nil
+}
+
+func ApplyRequestContext(ctx context.Context, body hcl.Body, req *http.Request) error {
 	if req == nil {
 		return nil
 	}
 
-	// TODO: bufferOpts from parent
-	opts := BufferRequest
-	httpCtx := NewHTTPContext(ctx, opts, req)
+	var httpCtx *hcl.EvalContext
+	if c, ok := ctx.Value(ContextType).(*Context); ok {
+		httpCtx = c.eval
+	}
 
 	content, _, _ := body.PartialContent(meta.AttributesSchema)
 
@@ -138,15 +177,15 @@ func evalURLPath(req *http.Request, attrs map[string]*hcl.Attribute, httpCtx *hc
 	}
 }
 
-func ApplyResponseContext(ctx *hcl.EvalContext, body hcl.Body, req *http.Request, beresp *http.Response) error {
+func ApplyResponseContext(ctx context.Context, body hcl.Body, beresp *http.Response) error {
 	if beresp == nil {
 		return nil
 	}
 
-	// TODO: bufferOpts from parent
-	opts := BufferResponse
-	httpCtx := NewHTTPContext(ctx, opts, req, beresp)
-
+	var httpCtx *hcl.EvalContext
+	if c, ok := ctx.Value(ContextType).(*Context); ok {
+		httpCtx = c.eval
+	}
 	content, _, _ := body.PartialContent(meta.AttributesSchema)
 
 	// map to name
diff --git a/eval/reader.go b/eval/reader.go
new file mode 100644
index 00000000..5ab97adb
--- /dev/null
+++ b/eval/reader.go
@@ -0,0 +1,20 @@
+package eval
+
+import "io"
+
+type ReadCloser struct {
+	io.Reader
+	closer io.Closer
+}
+
+func NewReadCloser(r io.Reader, c io.Closer) *ReadCloser {
+	rc := &ReadCloser{Reader: r, closer: c}
+	return rc
+}
+
+func (rc ReadCloser) Close() error {
+	if rc.closer == nil {
+		return nil
+	}
+	return rc.closer.Close()
+}
diff --git a/eval/variables.go b/eval/variables.go
index 3ee9385f..0f0c1cbd 100644
--- a/eval/variables.go
+++ b/eval/variables.go
@@ -7,7 +7,7 @@ const (
 	BackendResponses = "beresps"
 	BackendDefault   = "default"
 	ClientRequest    = "req"
-	Context          = "ctx"
+	CTX              = "ctx"
 	Cookies          = "cookies"
 	Endpoint         = "endpoint"
 	Environment      = "env"
diff --git a/handler/endpoint.go b/handler/endpoint.go
index 04ff4cfd..f3569a75 100644
--- a/handler/endpoint.go
+++ b/handler/endpoint.go
@@ -1,14 +1,11 @@
 package handler
 
 import (
-	"bytes"
 	"context"
-	"io"
 	"net"
 	"net/http"
 	"strings"
 
-	"github.com/docker/go-units"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/sirupsen/logrus"
 
@@ -22,11 +19,9 @@ import (
 )
 
 var _ http.Handler = &Endpoint{}
-
-const defaultReqBodyLimit = "64MiB"
+var _ EndpointLimit = &Endpoint{}
 
 type Endpoint struct {
-	evalContext    *hcl.EvalContext
 	log            *logrus.Entry
 	logHandlerKind string
 	opts           *EndpointOptions
@@ -46,16 +41,19 @@ type EndpointOptions struct {
 	ServerOpts     *server.Options
 }
 
-func NewEndpoint(opts *EndpointOptions, evalCtx *hcl.EvalContext, log *logrus.Entry,
-	proxies producer.Proxies, requests producer.Requests, resp *producer.Response) *Endpoint {
+type EndpointLimit interface {
+	RequestLimit() int64
+}
+
+func NewEndpoint(opts *EndpointOptions, log *logrus.Entry, proxies producer.Proxies,
+	requests producer.Requests, resp *producer.Response) *Endpoint {
 	opts.ReqBufferOpts |= eval.MustBuffer(opts.Context) // TODO: proper configuration on all hcl levels
 	return &Endpoint{
-		evalContext: evalCtx,
-		log:         log.WithField("handler", opts.LogHandlerKind),
-		opts:        opts,
-		proxies:     proxies,
-		requests:    requests,
-		response:    resp,
+		log:      log.WithField("handler", opts.LogHandlerKind),
+		opts:     opts,
+		proxies:  proxies,
+		requests: requests,
+		response: resp,
 	}
 }
 
@@ -69,12 +67,7 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	subCtx, cancel := context.WithCancel(reqCtx)
 	defer cancel()
 
-	if err := e.SetGetBody(req); err != nil {
-		e.opts.Error.ServeError(err).ServeHTTP(rw, req)
-		return
-	}
-
-	if ee := eval.ApplyRequestContext(e.evalContext, e.opts.Context, req); ee != nil {
+	if ee := eval.ApplyRequestContext(req.Context(), e.opts.Context, req); ee != nil {
 		e.log.Error(ee)
 	}
 
@@ -82,10 +75,10 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	requestResults := make(producer.Results)
 
 	// go for it due to chan write on error
-	go e.proxies.Produce(subCtx, req, e.evalContext, proxyResults)
-	go e.requests.Produce(subCtx, req, e.evalContext, requestResults)
+	go e.proxies.Produce(subCtx, req, proxyResults)
+	go e.requests.Produce(subCtx, req, requestResults)
 
-	beresps := make(map[string]*producer.Result)
+	beresps := make(producer.ResultMap)
 	// TODO: read parallel, proxy first for now
 	e.readResults(proxyResults, beresps)
 	e.readResults(requestResults, beresps)
@@ -93,11 +86,14 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	var clientres *http.Response
 	var err error
 
+	evalContext := req.Context().Value(eval.ContextType).(*eval.Context)
+	evalContext = evalContext.WithBeresps(beresps.List()...)
+
 	// assume prio or err on conf load if set with response
 	if e.redirect != nil {
 		clientres = e.newRedirect()
 	} else if e.response != nil {
-		clientres, err = e.newResponse(req, beresps)
+		clientres, err = e.newResponse(req, evalContext)
 	} else {
 		if result, ok := beresps["default"]; ok {
 			clientres = result.Beresp
@@ -121,7 +117,7 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	}
 
 	// always apply before write: redirect, response
-	if err = eval.ApplyResponseContext(e.evalContext, e.opts.Context, req, clientres); err != nil {
+	if err = eval.ApplyResponseContext(evalContext, e.opts.Context, clientres); err != nil {
 		e.log.Error(err)
 	}
 
@@ -130,46 +126,7 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	}
 }
 
-// SetGetBody determines if we have to buffer a request body for further processing.
-// First of all the user has a related reference within a related options context declaration.
-// Additionally the request body is nil or a NoBody type and the http method has no body restrictions like 'TRACE'.
-func (e *Endpoint) SetGetBody(req *http.Request) error {
-	if req.Method == http.MethodTrace {
-		return nil
-	}
-
-	// TODO: handle buffer options based on overall body context and reference
-	//if (e.opts.ReqBufferOpts & eval.BufferRequest) != eval.BufferRequest {
-	//	return nil
-	//}
-
-	if req.Body != nil && req.Body != http.NoBody && req.GetBody == nil {
-		buf := &bytes.Buffer{}
-		lr := io.LimitReader(req.Body, e.opts.ReqBodyLimit+1)
-		n, err := buf.ReadFrom(lr)
-		if err != nil {
-			return err
-		}
-
-		if n > e.opts.ReqBodyLimit {
-			return errors.EndpointReqBodySizeExceeded
-		}
-
-		bodyBytes := buf.Bytes()
-		req.GetBody = func() (io.ReadCloser, error) {
-			return eval.NewReadCloser(bytes.NewBuffer(bodyBytes), req.Body), nil
-		}
-	}
-
-	return nil
-}
-
-func (e *Endpoint) newResponse(req *http.Request, beresps map[string]*producer.Result) (*http.Response, error) {
-	var resps []*http.Response
-	for _, br := range beresps {
-		resps = append(resps, br.Beresp)
-	}
-
+func (e *Endpoint) newResponse(req *http.Request, evalCtx *eval.Context) (*http.Response, error) {
 	clientres := &http.Response{
 		Header:     make(http.Header),
 		Proto:      req.Proto,
@@ -178,7 +135,8 @@ func (e *Endpoint) newResponse(req *http.Request, beresps map[string]*producer.R
 		Request:    req,
 	}
 
-	httpCtx := eval.NewHTTPContext(e.evalContext, e.opts.ReqBufferOpts, req, resps...)
+	hclCtx := evalCtx.HCLContext()
+
 	content, _, diags := e.response.Context.PartialContent(config.ResponseInlineSchema)
 	if diags.HasErrors() {
 		return nil, diags
@@ -186,19 +144,19 @@ func (e *Endpoint) newResponse(req *http.Request, beresps map[string]*producer.R
 
 	statusCode := http.StatusOK
 	if attr, ok := content.Attributes["status"]; ok {
-		val, _ := attr.Expr.Value(httpCtx)
+		val, _ := attr.Expr.Value(hclCtx)
 		statusCode = int(seetie.ValueToInt(val))
 	}
 	clientres.StatusCode = statusCode
 	clientres.Status = http.StatusText(clientres.StatusCode)
 
 	if attr, ok := content.Attributes["headers"]; ok {
-		val, _ := attr.Expr.Value(httpCtx)
+		val, _ := attr.Expr.Value(hclCtx)
 		eval.SetHeader(val, clientres.Header)
 	}
 
 	if attr, ok := content.Attributes["body"]; ok {
-		val, _ := attr.Expr.Value(httpCtx)
+		val, _ := attr.Expr.Value(hclCtx)
 		r := strings.NewReader(seetie.ValueToString(val))
 		clientres.Body = eval.NewReadCloser(r, nil)
 	}
@@ -216,7 +174,7 @@ func (e *Endpoint) newRedirect() *http.Response {
 	}
 }
 
-func (e *Endpoint) readResults(requestResults producer.Results, beresps map[string]*producer.Result) {
+func (e *Endpoint) readResults(requestResults producer.Results, beresps producer.ResultMap) {
 	for r := range requestResults { // collect resps
 		if r == nil {
 			panic("implement nil result handling")
@@ -243,15 +201,11 @@ func (e *Endpoint) Options() *server.Options {
 	return e.opts.ServerOpts
 }
 
+func (e *Endpoint) RequestLimit() int64 {
+	return e.opts.ReqBodyLimit
+}
+
 // String interface maps to the access log handler field.
 func (e *Endpoint) String() string {
 	return e.logHandlerKind
 }
-
-func ParseBodyLimit(limit string) (int64, error) {
-	requestBodyLimit := defaultReqBodyLimit
-	if limit != "" {
-		requestBodyLimit = limit
-	}
-	return units.FromHumanSize(requestBodyLimit)
-}
diff --git a/handler/producer/produce.go b/handler/producer/produce.go
index 8589a5d7..9a57d75f 100644
--- a/handler/producer/produce.go
+++ b/handler/producer/produce.go
@@ -3,8 +3,6 @@ package producer
 import (
 	"context"
 	"net/http"
-
-	"github.com/hashicorp/hcl/v2"
 )
 
 var (
@@ -13,5 +11,5 @@ var (
 )
 
 type Roundtrips interface {
-	Produce(ctx context.Context, req *http.Request, evalCtx *hcl.EvalContext, results chan<- *Result)
+	Produce(ctx context.Context, req *http.Request, results chan<- *Result)
 }
diff --git a/handler/producer/proxy.go b/handler/producer/proxy.go
index 7b5ad846..c683a557 100644
--- a/handler/producer/proxy.go
+++ b/handler/producer/proxy.go
@@ -4,8 +4,6 @@ import (
 	"context"
 	"net/http"
 	"sync"
-
-	"github.com/hashicorp/hcl/v2"
 )
 
 type Proxy struct {
@@ -15,7 +13,7 @@ type Proxy struct {
 
 type Proxies []*Proxy
 
-func (pr Proxies) Produce(ctx context.Context, clientReq *http.Request, _ *hcl.EvalContext, results chan<- *Result) {
+func (pr Proxies) Produce(ctx context.Context, clientReq *http.Request, results chan<- *Result) {
 	wg := &sync.WaitGroup{}
 	wg.Add(len(pr))
 	go func() {
diff --git a/handler/producer/request.go b/handler/producer/request.go
index a0f5d426..b3a46de0 100644
--- a/handler/producer/request.go
+++ b/handler/producer/request.go
@@ -26,7 +26,7 @@ type Request struct {
 // Requests represents the producer <Requests> object.
 type Requests []*Request
 
-func (r Requests) Produce(ctx context.Context, _ *http.Request, evalCtx *hcl.EvalContext, results chan<- *Result) {
+func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<- *Result) {
 	wg := &sync.WaitGroup{}
 	wg.Add(len(r))
 	go func() {
@@ -34,23 +34,23 @@ func (r Requests) Produce(ctx context.Context, _ *http.Request, evalCtx *hcl.Eva
 		close(results)
 	}()
 
-	for _, req := range r {
-		outreq, err := http.NewRequest(req.Method, req.URL, strings.NewReader(req.Body))
+	for _, or := range r {
+		outreq, err := http.NewRequest(or.Method, or.URL, strings.NewReader(or.Body))
 		if err != nil {
 			results <- &Result{Err: err}
 			wg.Done()
 			continue
 		}
 
-		outCtx := withRoundTripName(ctx, req.Name)
-		err = eval.ApplyRequestContext(evalCtx, req.Context, outreq)
+		outCtx := withRoundTripName(ctx, or.Name)
+		err = eval.ApplyRequestContext(req.Context(), or.Context, outreq)
 		if err != nil {
 			results <- &Result{Err: err}
 			wg.Done()
 			continue
 		}
 		*outreq = *outreq.WithContext(outCtx)
-		go roundtrip(req.Backend, outreq, results, wg)
+		go roundtrip(or.Backend, outreq, results, wg)
 	}
 }
 
diff --git a/handler/producer/result.go b/handler/producer/result.go
index f9b251ce..4d6d0c96 100644
--- a/handler/producer/result.go
+++ b/handler/producer/result.go
@@ -15,6 +15,16 @@ type Result struct {
 // Results represents the producer <Result> channel.
 type Results chan *Result
 
+type ResultMap map[string]*Result
+
+func (rm ResultMap) List() []*http.Response {
+	var list []*http.Response
+	for _, br := range rm {
+		list = append(list, br.Beresp)
+	}
+	return list
+}
+
 func roundtrip(rt http.RoundTripper, req *http.Request, results chan<- *Result, wg *sync.WaitGroup) {
 	defer wg.Done()
 
diff --git a/handler/proxy.go b/handler/proxy.go
index 0dcbbf5f..2d11d316 100644
--- a/handler/proxy.go
+++ b/handler/proxy.go
@@ -17,15 +17,13 @@ import (
 type Proxy struct {
 	backend      http.RoundTripper
 	context      hcl.Body
-	evalCtx      *hcl.EvalContext
 	reverseProxy *httputil.ReverseProxy
 }
 
-func NewProxy(backend http.RoundTripper, ctx hcl.Body, evalCtx *hcl.EvalContext) *Proxy {
+func NewProxy(backend http.RoundTripper, ctx hcl.Body) *Proxy {
 	proxy := &Proxy{
 		backend: backend,
 		context: ctx,
-		evalCtx: evalCtx,
 	}
 	rp := &httputil.ReverseProxy{
 		Director:  proxy.director,
@@ -41,7 +39,7 @@ func NewProxy(backend http.RoundTripper, ctx hcl.Body, evalCtx *hcl.EvalContext)
 }
 
 func (p *Proxy) RoundTrip(req *http.Request) (*http.Response, error) {
-	if err := eval.ApplyRequestContext(p.evalCtx, p.context, req); err != nil {
+	if err := eval.ApplyRequestContext(req.Context(), p.context, req); err != nil {
 		return nil, err // TODO: log only
 	}
 
@@ -53,7 +51,7 @@ func (p *Proxy) RoundTrip(req *http.Request) (*http.Response, error) {
 	if err != nil {
 		return beresp, err
 	}
-	err = eval.ApplyResponseContext(p.evalCtx, p.context, req, beresp) // TODO: log only
+	err = eval.ApplyResponseContext(req.Context(), p.context, beresp) // TODO: log only
 	return beresp, err
 }
 
diff --git a/handler/transport/backend.go b/handler/transport/backend.go
index 9a22744d..42fdeaba 100644
--- a/handler/transport/backend.go
+++ b/handler/transport/backend.go
@@ -37,7 +37,6 @@ var ReClientSupportsGZ = regexp.MustCompile(`(?i)\b` + GzipName + `\b`)
 type Backend struct {
 	accessControl    string // maps to basic-auth atm
 	context          hcl.Body
-	evalContext      *hcl.EvalContext
 	name             string
 	openAPIValidator *validation.OpenAPI
 	transportConf    *Config
@@ -48,14 +47,13 @@ type Backend struct {
 }
 
 // NewBackend creates a new <*Backend> object by the given <*Config>.
-func NewBackend(evalCtx *hcl.EvalContext, ctx hcl.Body, conf *Config, log *logrus.Entry, openAPIopts *validation.OpenAPIOptions) http.RoundTripper {
+func NewBackend(ctx hcl.Body, conf *Config, log *logrus.Entry, openAPIopts *validation.OpenAPIOptions) http.RoundTripper {
 	logEntry := log
 	if conf.BackendName != "" {
 		logEntry = log.WithField("backend", conf.BackendName)
 	}
 
 	backend := &Backend{
-		evalContext:      evalCtx,
 		context:          ctx,
 		openAPIValidator: validation.NewOpenAPI(openAPIopts),
 		transportConf:    conf,
@@ -66,15 +64,15 @@ func NewBackend(evalCtx *hcl.EvalContext, ctx hcl.Body, conf *Config, log *logru
 
 // RoundTrip implements the <http.RoundTripper> interface.
 func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
+	tc := b.evalTransport(req)
+	t := Get(tc)
+
 	if b.transportConf.Timeout > 0 {
 		deadline, cancel := context.WithTimeout(req.Context(), b.transportConf.Timeout)
 		defer cancel()
 		*req = *req.WithContext(deadline)
 	}
 
-	tc := b.evalTransport(req)
-	t := Get(tc)
-
 	if req.URL.Scheme == "" {
 		req.URL.Scheme = tc.Scheme
 	}
@@ -82,7 +80,7 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 	req.URL.Host = tc.Origin
 	req.Host = tc.Hostname
 
-	err := eval.ApplyRequestContext(b.evalContext, b.context, req)
+	err := eval.ApplyRequestContext(req.Context(), b.context, req)
 	if err != nil {
 		return nil, err
 	}
@@ -138,13 +136,24 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 		removeConnectionHeaders(req.Header)
 	}
 
-	err = eval.ApplyResponseContext(b.evalContext, b.context, req, beresp)
-	beresp.Request = req
+	// Backend response context creates the beresp variables in first place and applies this context
+	// to the current beresp obj. Downstream response context evals reading their beresp variable values
+	// from this result. Fallback case is for testing purposes.
+	if evalCtx, ok := req.Context().Value(eval.ContextType).(*eval.Context); ok {
+		evalCtx = evalCtx.WithBeresps(beresp)
+		err = eval.ApplyResponseContext(evalCtx, b.context, beresp)
+	} else {
+		err = eval.ApplyResponseContext(req.Context(), b.context, beresp)
+	}
+
 	return beresp, err
 }
 
 func (b *Backend) evalTransport(req *http.Request) *Config {
-	httpContext := eval.NewHTTPContext(b.evalContext, eval.BufferNone, req)
+	var httpContext *hcl.EvalContext
+	if httpCtx, ok := req.Context().Value(eval.ContextType).(*eval.Context); ok {
+		httpContext = httpCtx.HCLContext()
+	}
 	content, _, diags := b.context.PartialContent(config.BackendInlineSchema)
 	if diags.HasErrors() {
 		b.upstreamLog.LogEntry().Error(diags)
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index 854d1d90..2fba813c 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -36,7 +36,7 @@ func ValueToMap(val cty.Value) map[string]interface{} {
 	}
 
 	for k, v := range valMap {
-		if v.IsNull() || !v.IsKnown() {
+		if v.IsNull() || !v.IsWhollyKnown() {
 			result[k] = ""
 			continue
 		}
@@ -50,6 +50,8 @@ func ValueToMap(val cty.Value) map[string]interface{} {
 		case cty.Number:
 			f, _ := v.AsBigFloat().Float64()
 			result[k] = f
+		case cty.Map(cty.NilType):
+			result[k] = ""
 		default:
 			if isTuple(v) {
 				result[k] = ValueToStringSlice(v)
diff --git a/server/http.go b/server/http.go
index 225c8691..68cfc4ec 100644
--- a/server/http.go
+++ b/server/http.go
@@ -11,10 +11,12 @@ import (
 	uuid "github.com/satori/go.uuid"
 	"github.com/sirupsen/logrus"
 
+	ac "github.com/avenga/couper/accesscontrol"
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/env"
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/config/runtime"
+	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
 	"github.com/avenga/couper/handler/transport"
 	"github.com/avenga/couper/internal/test"
@@ -25,23 +27,24 @@ import (
 type HTTPServer struct {
 	accessLog  *logging.AccessLog
 	commandCtx context.Context
-	settings   *config.Settings
-	timings    *runtime.HTTPTimings
+	evalCtx    *eval.Context
 	listener   net.Listener
 	log        logrus.FieldLogger
 	mux        *Mux
 	port       string
+	settings   *config.Settings
 	shutdownCh chan struct{}
 	srv        *http.Server
+	timings    *runtime.HTTPTimings
 	uidFn      func() string
 }
 
 // NewServerList creates a list of all configured HTTP server.
-func NewServerList(cmdCtx context.Context, log logrus.FieldLogger, settings *config.Settings, timings *runtime.HTTPTimings, srvConf runtime.ServerConfiguration) ([]*HTTPServer, func()) {
+func NewServerList(cmdCtx context.Context, evalCtx *eval.Context, log logrus.FieldLogger, settings *config.Settings, timings *runtime.HTTPTimings, srvConf runtime.ServerConfiguration) ([]*HTTPServer, func()) {
 	var list []*HTTPServer
 
 	for port, srvMux := range srvConf {
-		list = append(list, New(cmdCtx, log, settings, timings, port, srvMux))
+		list = append(list, New(cmdCtx, evalCtx, log, settings, timings, port, srvMux))
 	}
 
 	handleShutdownFn := func() {
@@ -53,7 +56,7 @@ func NewServerList(cmdCtx context.Context, log logrus.FieldLogger, settings *con
 }
 
 // New creates a configured HTTP server.
-func New(cmdCtx context.Context, log logrus.FieldLogger, settings *config.Settings, timings *runtime.HTTPTimings, p runtime.Port, muxOpts *runtime.MuxOptions) *HTTPServer {
+func New(cmdCtx context.Context, evalCtx *eval.Context, log logrus.FieldLogger, settings *config.Settings, timings *runtime.HTTPTimings, p runtime.Port, muxOpts *runtime.MuxOptions) *HTTPServer {
 	var uidFn func() string
 	if settings.RequestIDFormat == "uuid4" {
 		uidFn = func() string {
@@ -75,6 +78,7 @@ func New(cmdCtx context.Context, log logrus.FieldLogger, settings *config.Settin
 	mux.MustAddRoute(http.MethodGet, settings.HealthPath, handler.NewHealthCheck(settings.HealthPath, shutdownCh))
 
 	httpSrv := &HTTPServer{
+		evalCtx:    evalCtx,
 		accessLog:  logging.NewAccessLog(&logConf, log),
 		commandCtx: cmdCtx,
 		log:        log,
@@ -176,11 +180,33 @@ func (s *HTTPServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	)
 	rw = w
 
-	s.accessLog.ServeHTTP(rw, req, h, startTime)
+	if err := s.setGetBody(h, req); err != nil {
+		s.mux.opts.ErrorTpl.ServeError(err).ServeHTTP(rw, req)
+		return
+	}
+
+	ctx = s.evalCtx.WithClientRequest(req)
+	clientReq := req.Clone(ctx)
+
+	s.accessLog.ServeHTTP(rw, clientReq, h, startTime)
 
 	w.Close() // Closes the GZ writer.
 }
 
+func (s *HTTPServer) setGetBody(h http.Handler, req *http.Request) error {
+	outer := h
+	if inner, protected := outer.(ac.ProtectedHandler); protected {
+		outer = inner.Child()
+	}
+
+	if limitHandler, ok := h.(handler.EndpointLimit); ok {
+		if err := eval.SetGetBody(req, limitHandler.RequestLimit()); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // getHost configures the host from the incoming request host based on
 // the xfh setting and listener port to be prepared for the http multiplexer.
 func (s *HTTPServer) getHost(req *http.Request) string {


Test patch:
diff --git a/eval/context_test.go b/eval/context_test.go
index c5054960..3aa13321 100644
--- a/eval/context_test.go
+++ b/eval/context_test.go
@@ -11,12 +11,10 @@ import (
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclsimple"
-	logrustest "github.com/sirupsen/logrus/hooks/test"
 	"github.com/zclconf/go-cty/cty"
 
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/eval"
-	"github.com/avenga/couper/handler"
 	"github.com/avenga/couper/internal/seetie"
 	"github.com/avenga/couper/internal/test"
 )
@@ -33,7 +31,7 @@ func TestNewHTTPContext(t *testing.T) {
 		}
 	}
 
-	baseCtx := eval.NewENVContext(nil)
+	baseCtx := eval.NewContext(nil)
 
 	tests := []struct {
 		name      string
@@ -41,7 +39,7 @@ func TestNewHTTPContext(t *testing.T) {
 		reqHeader http.Header
 		body      io.Reader
 		query     string
-		baseCtx   *hcl.EvalContext
+		baseCtx   *eval.Context
 		hcl       string
 		want      http.Header
 	}{
@@ -84,13 +82,8 @@ func TestNewHTTPContext(t *testing.T) {
 		`, http.Header{"method": {http.MethodGet}, "title": {""}}},
 	}
 
-	logger, hook := logrustest.NewNullLogger()
-	log := logger.WithContext(context.Background())
-
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			hook.Reset()
-
 			helper := test.New(t)
 
 			req := httptest.NewRequest(tt.reqMethod, "https://couper.io/"+tt.query, tt.body)
@@ -103,15 +96,9 @@ func TestNewHTTPContext(t *testing.T) {
 			bereq := req.Clone(context.Background())
 			beresp := newBeresp(bereq)
 
-			endpointHandler := handler.NewEndpoint(&handler.EndpointOptions{
-				Context:       helper.NewProxyContext(tt.hcl),
-				ReqBodyLimit:  512,
-				ReqBufferOpts: eval.BufferRequest,
-			}, eval.NewENVContext(nil), log, nil, nil, nil)
-
-			helper.Must(endpointHandler.SetGetBody(req))
+			helper.Must(eval.SetGetBody(req, 512))
 
-			ctx := eval.NewHTTPContext(tt.baseCtx, eval.BufferRequest, req, beresp)
+			ctx := baseCtx.WithClientRequest(req).WithBeresps(beresp).HCLContext()
 			ctx.Functions = nil // we are not interested in a functions test
 
 			var resultMap map[string]cty.Value
diff --git a/eval/http_test.go b/eval/http_test.go
new file mode 100644
index 00000000..2dd09569
--- /dev/null
+++ b/eval/http_test.go
@@ -0,0 +1,37 @@
+package eval_test
+
+import (
+	"bytes"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"testing"
+
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/eval"
+)
+
+func Test_SetGetBody_LimitBody(t *testing.T) {
+	type testCase struct {
+		name    string
+		limit   int64
+		payload string
+		wantErr error
+	}
+
+	for _, testcase := range []testCase{
+		{"/w well sized limit", 1024, "content", nil},
+		{"/w zero limit", 0, "01", errors.EndpointReqBodySizeExceeded},
+		{"/w limit /w oversize body", 4, "12345", errors.EndpointReqBodySizeExceeded},
+	} {
+		t.Run(testcase.name, func(subT *testing.T) {
+			req := httptest.NewRequest(http.MethodPut, "/", bytes.NewBufferString(testcase.payload))
+
+			err := eval.SetGetBody(req, testcase.limit)
+
+			if !reflect.DeepEqual(err, testcase.wantErr) {
+				subT.Errorf("Expected '%v', got: '%v'", testcase.wantErr, err)
+			}
+		})
+	}
+}
diff --git a/eval/lib/time_test.go b/eval/lib/time_test.go
index cf7e7217..871db8f0 100644
--- a/eval/lib/time_test.go
+++ b/eval/lib/time_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/zclconf/go-cty/cty"
 
 	"github.com/avenga/couper/config/configload"
+	"github.com/avenga/couper/internal/test"
 )
 
 func TestUnixtime(t *testing.T) {
@@ -27,14 +28,16 @@ func TestUnixtime(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			helper := test.New(t)
 			cf, err := configload.LoadBytes([]byte(tt.hcl), "couper.hcl")
-			now, err := cf.Context.Functions["unixtime"].Call([]cty.Value{})
-			if err != nil {
-				t.Fatal(err)
-			}
+			helper.Must(err)
+			now, err := cf.Context.HCLContext().Functions["unixtime"].Call([]cty.Value{})
+			helper.Must(err)
+
 			if !cty.Number.Equals(now.Type()) {
 				t.Errorf("Wrong return type; expected %s, got: %s", cty.Number.FriendlyName(), now.Type().FriendlyName())
 			}
+
 			bfnow := now.AsBigFloat()
 			inow, _ := bfnow.Int64()
 			if inow != tt.want {
diff --git a/handler/endpoint_test.go b/handler/endpoint_test.go
index 387f6863..f21f9db0 100644
--- a/handler/endpoint_test.go
+++ b/handler/endpoint_test.go
@@ -3,12 +3,10 @@ package handler_test
 import (
 	"bytes"
 	"context"
-	"fmt"
 	"io"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
-	"reflect"
 	"strings"
 	"testing"
 
@@ -16,7 +14,6 @@ import (
 	"github.com/hashicorp/hcl/v2/hclsimple"
 	logrustest "github.com/sirupsen/logrus/hooks/test"
 
-	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
@@ -26,40 +23,6 @@ import (
 	"github.com/avenga/couper/server"
 )
 
-func TestEndpoint_RoundTrip_SetGetBody_LimitBody(t *testing.T) {
-	type testCase struct {
-		name    string
-		limit   string
-		payload string
-		wantErr error
-	}
-
-	for _, testcase := range []testCase{
-		{"/w well sized limit", "12MiB", "content", nil},
-		{"/w zero limit", "0", "01", errors.EndpointReqBodySizeExceeded},
-		{"/w limit /w oversize body", "4B", "12345", errors.EndpointReqBodySizeExceeded},
-	} {
-		t.Run(testcase.name, func(subT *testing.T) {
-			helper := test.New(subT)
-
-			logger, _ := logrustest.NewNullLogger()
-			log := logger.WithContext(context.Background())
-
-			bodyLimit, _ := handler.ParseBodyLimit(testcase.limit)
-			epHandler := handler.NewEndpoint(&handler.EndpointOptions{
-				Context:      helper.NewProxyContext("set_request_headers = { x = req.post }"),
-				ReqBodyLimit: bodyLimit,
-			}, eval.NewENVContext(nil), log, nil, nil, nil)
-
-			req := httptest.NewRequest(http.MethodPut, "/", bytes.NewBufferString(testcase.payload))
-			err := epHandler.SetGetBody(req)
-			if !reflect.DeepEqual(err, testcase.wantErr) {
-				subT.Errorf("Expected '%v', got: '%v'", testcase.wantErr, err)
-			}
-		})
-	}
-}
-
 func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 	type header map[string]string
 
@@ -71,8 +34,6 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 		wantHeader header
 	}
 
-	baseCtx := eval.NewENVContext(nil)
-
 	type hclBody struct {
 		Inline hcl.Body `hcl:",remain"`
 	}
@@ -106,7 +67,7 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 		}},
 	}
 
-	evalCtx := eval.NewENVContext(nil)
+	evalCtx := eval.NewContext(nil)
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
@@ -114,10 +75,10 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 			hook.Reset()
 
 			var remain hclBody
-			err := hclsimple.Decode("test.hcl", []byte(tt.hcl), baseCtx, &remain)
+			err := hclsimple.Decode("test.hcl", []byte(tt.hcl), evalCtx.HCLContext(), &remain)
 			helper.Must(err)
 
-			backend := transport.NewBackend(evalCtx,
+			backend := transport.NewBackend(
 				test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 				&transport.Config{NoProxyFromEnv: true}, logger, nil)
 
@@ -125,16 +86,18 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 				Error:        errors.DefaultJSON,
 				Context:      remain.Inline,
 				ReqBodyLimit: 1024,
-			}, evalCtx, logger, producer.Proxies{
+			}, logger, producer.Proxies{
 				&producer.Proxy{Name: "default", RoundTrip: backend},
 			}, nil, nil)
 
 			req := httptest.NewRequest(tt.method, "http://couper.io", tt.body)
-
 			if tt.body != nil {
 				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 			}
 
+			helper.Must(eval.SetGetBody(req, 1024))
+			*req = *req.WithContext(evalCtx.WithClientRequest(req))
+
 			rec := httptest.NewRecorder()
 			rw := server.NewRWWrapper(rec, false) // crucial for working ep due to res.Write()
 			ep.ServeHTTP(rw, req)
@@ -170,13 +133,13 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
 
 	defaultMethods := []string{
 		http.MethodGet,
-		http.MethodHead,
-		http.MethodPost,
-		http.MethodPut,
-		http.MethodPatch,
-		http.MethodDelete,
-		http.MethodConnect,
-		http.MethodOptions,
+		//http.MethodHead,
+		//http.MethodPost,
+		//http.MethodPut,
+		//http.MethodPatch,
+		//http.MethodDelete,
+		//http.MethodConnect,
+		//http.MethodOptions,
 	}
 
 	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@@ -210,8 +173,6 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
 			test.Header{"Content-Type": "application/json"}, "", want{req: test.Header{"x-test": ""}}},
 	}
 
-	evalCtx := eval.NewENVContext(nil)
-
 	log, _ := logrustest.NewNullLogger()
 	logger := log.WithContext(context.Background())
 
@@ -220,7 +181,7 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
 			t.Run(method+" "+tt.name, func(subT *testing.T) {
 				helper := test.New(subT)
 
-				backend := transport.NewBackend(evalCtx,
+				backend := transport.NewBackend(
 					helper.NewProxyContext(tt.inlineCtx),
 					&transport.Config{NoProxyFromEnv: true}, logger, nil)
 
@@ -228,7 +189,7 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
 					Error:        errors.DefaultJSON,
 					Context:      hcl.EmptyBody(),
 					ReqBodyLimit: 1024,
-				}, evalCtx, logger, producer.Proxies{
+				}, logger, producer.Proxies{
 					&producer.Proxy{Name: "default", RoundTrip: backend},
 				}, nil, nil)
 
@@ -239,6 +200,10 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
 				req := httptest.NewRequest(method, "/", body)
 				tt.header.Set(req)
 
+				// normally injected by server/http
+				helper.Must(eval.SetGetBody(req, 1024))
+				*req = *req.WithContext(eval.NewContext(nil).WithClientRequest(req))
+
 				rec := httptest.NewRecorder()
 				rw := server.NewRWWrapper(rec, false) // crucial for working ep due to res.Write()
 				ep.ServeHTTP(rw, req)
@@ -282,12 +247,10 @@ func TestEndpoint_RoundTripContext_Null_Eval(t *testing.T) {
 		helper.Must(err)
 	}))
 
-	evalCtx := eval.NewENVContext(nil)
-
 	log, _ := logrustest.NewNullLogger()
 	logger := log.WithContext(context.Background())
 
-	for i, tc := range []testCase{
+	for _, tc := range []testCase{
 		{"no eval", `path = "/"`, test.Header{}},
 		{"json_body client field", `set_response_headers = { "x-client" = "my-val-x-${req.json_body.client}" }`,
 			test.Header{
@@ -303,7 +266,7 @@ func TestEndpoint_RoundTripContext_Null_Eval(t *testing.T) {
 		t.Run(tc.name, func(st *testing.T) {
 			h := test.New(st)
 
-			backend := transport.NewBackend(evalCtx,
+			backend := transport.NewBackend(
 				test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 				&transport.Config{NoProxyFromEnv: true}, logger, nil)
 
@@ -311,13 +274,16 @@ func TestEndpoint_RoundTripContext_Null_Eval(t *testing.T) {
 				Error:        errors.DefaultJSON,
 				Context:      helper.NewProxyContext(tc.remain),
 				ReqBodyLimit: 1024,
-			}, evalCtx, logger, producer.Proxies{
+			}, logger, producer.Proxies{
 				&producer.Proxy{Name: "default", RoundTrip: backend},
 			}, nil, nil)
 
 			req := httptest.NewRequest(http.MethodGet, "http://localhost/", bytes.NewReader(clientPayload))
 			req.Header.Set("Content-Type", "application/json")
-			*req = *req.WithContext(context.WithValue(req.Context(), request.UID, fmt.Sprintf("#%.2d: %s", i+1, tc.name)))
+
+			helper.Must(eval.SetGetBody(req, 1024))
+			ctx := eval.NewContext(nil).WithClientRequest(req)
+			*req = *req.WithContext(ctx)
 
 			rec := httptest.NewRecorder()
 			rw := server.NewRWWrapper(rec, false) // crucial for working ep due to res.Write()
diff --git a/handler/transport/backend_test.go b/handler/transport/backend_test.go
index c50dd44c..336df313 100644
--- a/handler/transport/backend_test.go
+++ b/handler/transport/backend_test.go
@@ -59,7 +59,7 @@ func TestBackend_RoundTrip_Timings(t *testing.T) {
 			hook.Reset()
 
 			tt.tconf.NoProxyFromEnv = true // use origin addr from transport.Config
-			backend := transport.NewBackend(eval.NewENVContext(nil), tt.context, tt.tconf, log, nil)
+			backend := transport.NewBackend(tt.context, tt.tconf, log, nil)
 
 			_, err := backend.RoundTrip(tt.req)
 			if err != nil && tt.expectedErr == "" {
@@ -95,7 +95,7 @@ func TestBackend_Compression_Disabled(t *testing.T) {
 			"origin": hcltest.MockExprLiteral(u),
 		}),
 	})
-	backend := transport.NewBackend(eval.NewENVContext(nil), hclBody, &transport.Config{}, log, nil)
+	backend := transport.NewBackend(hclBody, &transport.Config{}, log, nil)
 
 	req := httptest.NewRequest(http.MethodOptions, "http://1.2.3.4/", nil)
 	res, err := backend.RoundTrip(req)
@@ -136,7 +136,7 @@ func TestBackend_Compression_ModifyAcceptEncoding(t *testing.T) {
 		}),
 	})
 
-	backend := transport.NewBackend(eval.NewENVContext(nil), hclBody, &transport.Config{
+	backend := transport.NewBackend(hclBody, &transport.Config{
 		Origin: origin.URL,
 	}, log, nil)
 
@@ -236,7 +236,7 @@ func TestBackend_RoundTrip_Validation(t *testing.T) {
 				origin = "` + origin.URL + `"
 			`)
 
-			backend := transport.NewBackend(eval.NewENVContext(nil), content, &transport.Config{}, log, openapiValidatorOptions)
+			backend := transport.NewBackend(content, &transport.Config{}, log, openapiValidatorOptions)
 
 			req := httptest.NewRequest(tt.requestMethod, "http://1.2.3.4"+tt.requestPath, nil)
 
@@ -311,7 +311,7 @@ func TestBackend_director(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			hclContext := helper.NewProxyContext(tt.inlineCtx)
 
-			backend := transport.NewBackend(eval.NewENVContext(nil), hclContext, &transport.Config{
+			backend := transport.NewBackend(hclContext, &transport.Config{
 				Timeout: time.Second,
 			}, nullLog, nil)
 
@@ -326,7 +326,7 @@ func TestBackend_director(t *testing.T) {
 			if !ok && tt.expReq.Host != req.Host {
 				t.Errorf("expected same host value, want: %q, got: %q", req.Host, tt.expReq.Host)
 			} else if ok {
-				hostVal, _ := hostnameExp.Expr.Value(eval.NewENVContext(nil))
+				hostVal, _ := hostnameExp.Expr.Value(eval.NewContext(nil).HCLContext())
 				hostname := seetie.ValueToString(hostVal)
 				if hostname != tt.expReq.Host {
 					t.Errorf("expected a configured request host: %q, got: %q", hostname, tt.expReq.Host)
@@ -396,7 +396,7 @@ func TestProxy_BufferingOptions(t *testing.T) {
 		t.Run(tc.name, func(st *testing.T) {
 			h := test.New(st)
 
-			backend := transport.NewBackend(eval.NewENVContext(nil), configload.MergeBodies([]hcl.Body{
+			backend := transport.NewBackend(configload.MergeBodies([]hcl.Body{
 				test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 				helper.NewProxyContext(tc.remain),
 			}), &transport.Config{}, nullLog, newOptions())
diff --git a/handler/validation/openapi_test.go b/handler/validation/openapi_test.go
index 49e50b24..0a8987b4 100644
--- a/handler/validation/openapi_test.go
+++ b/handler/validation/openapi_test.go
@@ -17,7 +17,6 @@ import (
 
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/body"
-	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler/transport"
 	"github.com/avenga/couper/handler/validation"
 	"github.com/avenga/couper/internal/test"
@@ -60,9 +59,7 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) {
 	openAPI, err := validation.NewOpenAPIOptions(beConf.OpenAPI)
 	helper.Must(err)
 
-	backend := transport.NewBackend(
-		eval.NewENVContext(nil), beConf.Remain, &transport.Config{}, logger, openAPI,
-	)
+	backend := transport.NewBackend(beConf.Remain, &transport.Config{}, logger, openAPI)
 
 	tests := []struct {
 		name, path string
diff --git a/internal/test/helper_proxy.go b/internal/test/helper_proxy.go
index 66a43de8..7401aac6 100644
--- a/internal/test/helper_proxy.go
+++ b/internal/test/helper_proxy.go
@@ -2,6 +2,7 @@ package test
 
 import (
 	"context"
+
 	"github.com/avenga/couper/handler/transport"
 
 	"github.com/hashicorp/hcl/v2"
@@ -29,10 +30,9 @@ func (h *Helper) NewProxy(conf *transport.Config, backendContext, proxyContext h
 		proxyCtx = hcl.EmptyBody()
 	}
 
-	evalCtx := eval.NewENVContext(nil)
-	backend := transport.NewBackend(evalCtx, backendContext, config, logger.WithContext(context.Background()), nil)
+	backend := transport.NewBackend(backendContext, config, logger.WithContext(context.Background()), nil)
 
-	proxy := handler.NewProxy(backend, proxyCtx, evalCtx)
+	proxy := handler.NewProxy(backend, proxyCtx)
 	return proxy
 }
 
@@ -42,6 +42,6 @@ func (h *Helper) NewProxyContext(inlineHCL string) hcl.Body {
 	}
 
 	var remain hclBody
-	h.Must(hclsimple.Decode(h.tb.Name()+".hcl", []byte(inlineHCL), eval.NewENVContext(nil), &remain))
+	h.Must(hclsimple.Decode(h.tb.Name()+".hcl", []byte(inlineHCL), eval.NewContext(nil).HCLContext(), &remain))
 	return configload.MergeBodies([]hcl.Body{remain.Inline})
 }
diff --git a/server/http_test.go b/server/http_test.go
index 6f4b1f09..72764e7b 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -14,6 +14,8 @@ import (
 	"text/template"
 	"time"
 
+	"github.com/avenga/couper/eval"
+
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/configload"
 
@@ -70,7 +72,7 @@ func TestHTTPServer_ServeHTTP_Files(t *testing.T) {
 	helper.Must(err)
 
 	port := runtime.Port(conf.Settings.DefaultPort)
-	gw := server.New(ctx, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, port, srvConf[port])
+	gw := server.New(ctx, conf.Context, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, port, srvConf[port])
 	gw.Listen()
 	defer gw.Close()
 
@@ -162,7 +164,7 @@ func TestHTTPServer_ServeHTTP_Files2(t *testing.T) {
 	srvConf, err := runtime.NewServerConfiguration(conf, log.WithContext(nil))
 	helper.Must(err)
 
-	couper := server.New(ctx, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, runtime.Port(0), srvConf[0])
+	couper := server.New(ctx, conf.Context, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, runtime.Port(0), srvConf[0])
 	couper.Listen()
 	defer couper.Close()
 
@@ -251,7 +253,7 @@ func TestHTTPServer_ServeHTTP_UUID_Option(t *testing.T) {
 			log, hook := logrustest.NewNullLogger()
 			settings := config.DefaultSettings
 			settings.RequestIDFormat = testcase.formatOption
-			srv := server.New(context.Background(), log, &settings, &runtime.DefaultTimings, 0, nil)
+			srv := server.New(context.Background(), eval.NewContext(nil), log, &settings, &runtime.DefaultTimings, 0, nil)
 			srv.Listen()
 			defer srv.Close()