Repository: avenga/couper
Pull request #158 (2021-03-24 08:36:12)

json_body, form_body


Patch:
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 2afaca39..548fc269 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -23,7 +23,7 @@ jobs:
       uses: actions/checkout@v2
 
     - name: Test
-      run: go test -v -timeout 120s -race ./...
+      run: go test -v -timeout 300s -race ./...
 
     - name: Build and push edge docker image
       if: github.event_name == 'push'
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 40ec1e71..a04be75c 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -30,7 +30,7 @@ jobs:
       run: go vet ./...
 
     - name: Test
-      run: go test -v -timeout 120s -race ./...
+      run: go test -v -timeout 300s -race ./...
 
     - name: Build binary
       run: go build -v -o couper .
diff --git a/Makefile b/Makefile
index 3608244c..f7bc2f7c 100644
--- a/Makefile
+++ b/Makefile
@@ -2,18 +2,18 @@ image:
 	docker build -t avenga/couper:latest .
 
 test:
-	go test -v -short -race -timeout 120s ./...
+	go test -v -short -race -timeout 300s ./...
 
 test-docker:
-	docker run --rm -v $(CURDIR):/go/app -w /go/app  golang sh -c "go test -short -count 10 -v -timeout 120s -race ./..."
+	docker run --rm -v $(CURDIR):/go/app -w /go/app  golang sh -c "go test -short -count 10 -v -timeout 300s -race ./..."
 
 test-coverage:
-	go test -short -timeout 120s -covermode=count -coverprofile=ac.coverage ./accesscontrol
-	go test -short -timeout 120s -covermode=count -coverprofile=eval.coverage ./eval
-	go test -short -timeout 120s -covermode=count -coverprofile=config.coverage ./config
-	go test -short -timeout 120s -covermode=count -coverprofile=handler.coverage ./handler
-	go test -short -timeout 120s -covermode=count -coverprofile=server.coverage ./server
-	go test -short -timeout 120s -covermode=count -coverprofile=main.coverage ./
+	go test -short -timeout 300s -covermode=count -coverprofile=ac.coverage ./accesscontrol
+	go test -short -timeout 300s -covermode=count -coverprofile=eval.coverage ./eval
+	go test -short -timeout 300s -covermode=count -coverprofile=config.coverage ./config
+	go test -short -timeout 300s -covermode=count -coverprofile=handler.coverage ./handler
+	go test -short -timeout 300s -covermode=count -coverprofile=server.coverage ./server
+	go test -short -timeout 300s -covermode=count -coverprofile=main.coverage ./
 	$(MAKE) test-coverage-show
 
 test-coverage-show:
diff --git a/config/backend.go b/config/backend.go
index 873de652..bbcff675 100644
--- a/config/backend.go
+++ b/config/backend.go
@@ -59,6 +59,10 @@ func (b Backend) Schema(inline bool) *hcl.BodySchema {
 }
 
 func newBackendSchema(schema *hcl.BodySchema, body hcl.Body) *hcl.BodySchema {
+	if body == nil {
+		return schema
+	}
+
 	for i, block := range schema.Blocks {
 		// Inline backend block MAY have no label.
 		if block.Type == "backend" && len(block.LabelNames) > 0 {
diff --git a/config/configload/load.go b/config/configload/load.go
index 7afdce21..322a69dc 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -313,6 +313,22 @@ func refineEndpoints(definedBackends Backends, endpoints config.Endpoints) error
 			if diags.HasErrors() {
 				return diags
 			}
+
+			_, 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,
+				}}
+			}
+
 			renameAttribute(content, "headers", "set_request_headers")
 			renameAttribute(content, "query_params", "set_query_params")
 
@@ -327,6 +343,19 @@ func refineEndpoints(definedBackends Backends, endpoints config.Endpoints) error
 			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()
diff --git a/config/request.go b/config/request.go
index 754c13cb..2a189c91 100644
--- a/config/request.go
+++ b/config/request.go
@@ -40,6 +40,8 @@ func (r Request) Schema(inline bool) *hcl.BodySchema {
 	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"`
diff --git a/config/response.go b/config/response.go
index 5e9ea6e9..6cc36eb4 100644
--- a/config/response.go
+++ b/config/response.go
@@ -31,9 +31,10 @@ func (r Response) Schema(inline bool) *hcl.BodySchema {
 	}
 
 	type Inline struct {
-		Body    string            `hcl:"body,optional"`
-		Headers map[string]string `hcl:"headers,optional"`
-		Status  int               `hcl:"status,optional"`
+		Body     string            `hcl:"body,optional"`
+		JsonBody string            `hcl:"json_body,optional"`
+		Headers  map[string]string `hcl:"headers,optional"`
+		Status   int               `hcl:"status,optional"`
 	}
 
 	schema, _ := gohcl.ImpliedBodySchema(&Inline{})
diff --git a/docs/README.md b/docs/README.md
index fd2f15ba..5345f182 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -205,7 +205,7 @@ since these references get evaluated at start.
 | `cookies.<name>`          | Value from `Cookie` request header for requested key (&#9888; last wins!) |
 | `query.<name>`            | Query parameter values (&#9888; last wins!) |
 | `path_params.<name>`      | Value from a named path parameter defined within an endpoint path label |
-| `post.<name>`             | Post form parameter |
+| `form_body.<name>`        | Parameter in a `application/x-www-form-urlencoded` body |
 | `json_body.<name>`        | Access json decoded object properties. Media type must be `application/json`. |
 | `ctx.<name>.<property_name>` | Request context containing claims from JWT used for [Access Control](#access-control) or information from a SAML assertion, `<name>` being the [JWT Block's](#jwt-block) or [SAML Block's](#saml-block) label and `property_name` being the claim's or assertion information's name |
 
@@ -219,7 +219,7 @@ since these references get evaluated at start.
 | `headers.<name>`          | HTTP request header value for requested lower-case key |
 | `cookies.<name>`          | Value from `Cookie` request header for requested key (&#9888; last wins!) |
 | `query.<name>`            | Query parameter values (&#9888; last wins!) |
-| `post.<name>`             | Post form parameter |
+| `form_body.<name>`        | Parameter in a `application/x-www-form-urlencoded` body |
 | `ctx.<name>.<property_name>` | Request context containing claims from JWT used for [Access Control](#access-control) or information from a SAML assertion, `<name>` being the [JWT Block's](#jwt-block) or [SAML Block's](#saml-block) label and `property_name` being the claim's or assertion information's name |
 | `url`                     | Backend origin URL |
 
@@ -437,7 +437,7 @@ produce an explicit or implicit client response.
 | [Request Block(s)](#request-block) |  |
 | [Response Block](#response-block)  |  |
 | **Attributes**                     | **Description** |
-| `request_body_limit`               | <ul><li>Optional.</li><li>Configures the maximum buffer size while accessing `req.post` or `req.json_body` content.</li><li>Valid units are: `KiB, MiB, GiB`.</li><li>Default limit is `64MiB`.</li></ul> |
+| `request_body_limit`               | <ul><li>Optional.</li><li>Configures the maximum buffer size while accessing `req.form_body` or `req.json_body` content.</li><li>Valid units are: `KiB, MiB, GiB`.</li><li>Default limit is `64MiB`.</li></ul> |
 | `path`                             | <ul><li>Optional.</li><li>Changeable part of the upstream URL.</li><li>Changes the path suffix of the outgoing request.</li></ul> |
 | `access_control`                   | <ul><li>Optional.</li><li>Sets predefined [Access Control](#access-control) for current `Endpoint Block` context.</li><li>*Example:* `access_control = ["foo"]`</li></ul> |
 | [Modifier](#modifier)              | <ul><li>Optional.</li><li>All [Modifier](#modifier).</li></ul> |
@@ -474,7 +474,9 @@ The `request` block creates and executes a request to a backend service.
 | **Attributes**                                      | **Description** |
 | [Backend Block Reference](#backend-block-reference) | <ul><li>&#9888; Mandatory if no [Backend Block](#backend-block) is defined.</li><li>References or refines a [Backend Block](#backend-block).</li></ul> |
 | `url`                                               | <ul><li>Optional.</li><li>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-reference) (if defined).</li></ul> |
-| `body`                                              | <ul><li>String.</li><li>Optional.</li></ul> |
+| `body`                                              | <ul><li>String.</li><li>Optional. Creates implicit default `Content-Type: text/plain` header field.</li></ul> |
+| `json_body`                                         | <ul><li>null, Boolean, Number, String, Object, or Tuple.</li><li>Optional. Creates implicit default `Content-Type: application/json` header field.</li></ul> |
+| `form_body`                                         | <ul><li>Object.</li><li>Optional. Creates implicit default `Content-Type: application/x-www-form-urlencoded` header field.</li></ul> |
 | `method`                                            | <ul><li>String.</li><li>Optional.</li><li>Default `GET`.</li></ul> |
 | `headers`                                           | <ul><li>Optional.</li><li>Same as `set_request_headers` in [Request Header](#request-header).</li></ul> |
 | `query_params`                                      | <ul><li>Optional.</li><li>Same as `set_query_params` in [Query Parameter](#query-parameter).</li></ul> |
@@ -488,7 +490,8 @@ The `response` block creates and sends a client response.
 | *context*      | [Endpoint Block](#endpoint-block). |
 | *label*        | Not implemented. |
 | **Attributes** | **Description** |
-| `body`         | <ul><li>String.</li><li>Optional.</li></ul> |
+| `body`         | <ul><li>String.</li><li>Optional. Creates implicit default `Content-Type: text/plain` header field.</li></ul> |
+| `json_body`    | <ul><li>null, Boolean, Number, String, Object, or Tuple.</li><li>Optional. Creates implicit default `Content-Type: application/json` header field.</li></ul> |
 | `status`       | <ul><li>HTTP status code.</li><li>Optional.</li><li>Default `200`.</li></ul> |
 | `headers`      | <ul><li>Optional.</li><li>Same as `set_response_headers` in [Request Header](#response-header).</li></ul> |
 
diff --git a/eval/buffer.go b/eval/buffer.go
index d3c2ebab..ca64bc9b 100644
--- a/eval/buffer.go
+++ b/eval/buffer.go
@@ -30,7 +30,7 @@ func (i BufferOption) GoString() string {
 	return strings.Join(result, "|")
 }
 
-// MustBuffer determines if any of the hcl.bodies makes use of 'post' or 'json_body'.
+// MustBuffer determines if any of the hcl.bodies makes use of 'form_body' or 'json_body'.
 func MustBuffer(body hcl.Body) BufferOption {
 	result := BufferNone
 
@@ -52,6 +52,10 @@ func MustBuffer(body hcl.Body) BufferOption {
 			nameField := reflect.ValueOf(traversal[1]).FieldByName("Name")
 			name := nameField.String()
 			switch name {
+			case FormBody:
+				if rootName == ClientRequest {
+					result |= BufferRequest
+				}
 			case JsonBody:
 				switch rootName {
 				case ClientRequest:
@@ -59,10 +63,6 @@ func MustBuffer(body hcl.Body) BufferOption {
 				case BackendResponse:
 					result |= BufferResponse
 				}
-			case Post:
-				if rootName == ClientRequest {
-					result |= BufferRequest
-				}
 			}
 		}
 	}
diff --git a/eval/context.go b/eval/context.go
index ff0835c1..4eba81de 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -104,12 +104,12 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {
 	}
 
 	ctx.eval.Variables[ClientRequest] = cty.ObjectVal(ctxMap.Merge(ContextMap{
+		FormBody:  seetie.ValuesMapToValue(parseForm(req).PostForm),
 		ID:        cty.StringVal(id),
 		JsonBody:  seetie.MapToValue(parseReqJSON(req)),
 		Method:    cty.StringVal(req.Method),
 		Path:      cty.StringVal(req.URL.Path),
 		PathParam: seetie.MapToValue(pathParams),
-		Post:      seetie.ValuesMapToValue(parseForm(req).PostForm),
 		Query:     seetie.ValuesMapToValue(req.URL.Query()),
 		URL:       cty.StringVal(newRawURL(req.URL).String()),
 	}.Merge(newVariable(ctx.inner, req.Cookies(), req.Header))))
@@ -141,11 +141,11 @@ func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
 			name = n
 		}
 		bereqs[name] = cty.ObjectVal(ContextMap{
-			Method: cty.StringVal(bereq.Method),
-			Path:   cty.StringVal(bereq.URL.Path),
-			Post:   seetie.ValuesMapToValue(parseForm(bereq).PostForm),
-			Query:  seetie.ValuesMapToValue(bereq.URL.Query()),
-			URL:    cty.StringVal(newRawURL(bereq.URL).String()),
+			FormBody: seetie.ValuesMapToValue(parseForm(bereq).PostForm),
+			Method:   cty.StringVal(bereq.Method),
+			Path:     cty.StringVal(bereq.URL.Path),
+			Query:    seetie.ValuesMapToValue(bereq.URL.Query()),
+			URL:      cty.StringVal(newRawURL(bereq.URL).String()),
 		}.Merge(newVariable(ctx.inner, bereq.Cookies(), bereq.Header)))
 
 		var jsonBody map[string]interface{}
@@ -214,12 +214,11 @@ const defaultMaxMemory = 32 << 20 // 32 MB
 // As Proxy we should not consume the request body.
 // Rewind body via GetBody method.
 func parseForm(r *http.Request) *http.Request {
-	if r.GetBody == nil {
+	if r.GetBody == nil || r.Form != nil {
 		return r
 	}
 	switch r.Method {
 	case http.MethodPut, http.MethodPatch, http.MethodPost:
-		r.Body, _ = r.GetBody() // rewind
 		_ = r.ParseMultipartForm(defaultMaxMemory)
 		r.Body, _ = r.GetBody() // reset
 	}
@@ -253,9 +252,8 @@ func parseReqJSON(req *http.Request) map[string]interface{} {
 		return nil
 	}
 
-	req.Body, _ = req.GetBody() // rewind
-	result := parseJSON(req.Body)
-	req.Body, _ = req.GetBody() // reset
+	body, _ := req.GetBody()
+	result := parseJSON(body)
 	return result
 }
 
diff --git a/eval/http.go b/eval/http.go
index 4ae26f04..5dc4cf38 100644
--- a/eval/http.go
+++ b/eval/http.go
@@ -3,13 +3,16 @@ package eval
 import (
 	"bytes"
 	"context"
+	er "errors"
 	"io"
 	"net/http"
+	"net/url"
 	"strconv"
 	"strings"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/zclconf/go-cty/cty"
+	"github.com/zclconf/go-cty/cty/function/stdlib"
 
 	"github.com/avenga/couper/config/meta"
 	"github.com/avenga/couper/config/request"
@@ -63,6 +66,12 @@ func SetGetBody(req *http.Request, bodyLimit int64) error {
 		req.GetBody = func() (io.ReadCloser, error) {
 			return io.NopCloser(bytes.NewBuffer(bodyBytes)), nil
 		}
+		// reset body initially, additional body reads which are not depending on http.Request
+		// internals like form parsing should just call GetBody() and use the returned reader.
+		req.Body, _ = req.GetBody()
+		// parsing form data now since they read/write request attributes which could be
+		// difficult with multiple routines later on.
+		parseForm(req)
 	}
 
 	return nil
@@ -265,6 +274,56 @@ func GetAttribute(ctx *hcl.EvalContext, content *hcl.BodyContent, name string) (
 	return seetie.ValueToString(val), nil
 }
 
+func GetBody(ctx *hcl.EvalContext, content *hcl.BodyContent) (string, string, error) {
+	attr, ok := content.Attributes["json_body"]
+	if ok {
+		val, err := attr.Expr.Value(ctx)
+		if err != nil {
+			return "", "", err
+		}
+
+		val, err1 := stdlib.JSONEncodeFunc.Call([]cty.Value{val})
+		if err1 != nil {
+			return "", "", err1
+		}
+
+		return val.AsString(), "application/json", nil
+	}
+
+	attr, ok = content.Attributes["form_body"]
+	if ok {
+		val, err := attr.Expr.Value(ctx)
+		if err != nil {
+			return "", "", err
+		}
+
+		if valType := val.Type(); !(valType.IsObjectType() || valType.IsMapType()) {
+			return "", "", er.New("value of form_body must be object")
+		}
+
+		data := url.Values{}
+		for k, v := range val.AsValueMap() {
+			for _, sv := range seetie.ValueToStringSlice(v) {
+				data.Add(k, sv)
+			}
+		}
+
+		return data.Encode(), "application/x-www-form-urlencoded", nil
+	}
+
+	attr, ok = content.Attributes["body"]
+	if ok {
+		val, err := attr.Expr.Value(ctx)
+		if err != nil {
+			return "", "", err
+		}
+
+		return seetie.ValueToString(val), "text/plain", nil
+	}
+
+	return "", "", nil
+}
+
 func SetHeader(val cty.Value, headerCtx http.Header) {
 	expMap := seetie.ValueToMap(val)
 	for key, v := range expMap {
diff --git a/eval/variables.go b/eval/variables.go
index 0f0c1cbd..f5dc28f4 100644
--- a/eval/variables.go
+++ b/eval/variables.go
@@ -11,6 +11,7 @@ const (
 	Cookies          = "cookies"
 	Endpoint         = "endpoint"
 	Environment      = "env"
+	FormBody         = "form_body"
 	Headers          = "headers"
 	HttpStatus       = "status"
 	ID               = "id"
@@ -18,7 +19,6 @@ const (
 	Method           = "method"
 	Path             = "path"
 	PathParam        = "path_params"
-	Post             = "post"
 	Query            = "query"
 	URL              = "url"
 )
diff --git a/handler/endpoint.go b/handler/endpoint.go
index fc087294..49240da4 100644
--- a/handler/endpoint.go
+++ b/handler/endpoint.go
@@ -187,6 +187,15 @@ func (e *Endpoint) newResponse(req *http.Request, evalCtx *eval.Context) (*http.
 	clientres.StatusCode = statusCode
 	clientres.Status = http.StatusText(clientres.StatusCode)
 
+	body, ct, err := eval.GetBody(hclCtx, content)
+	if err != nil {
+		e.log.Errorf("endpoint eval error: %v", err)
+	}
+
+	if ct != "" {
+		clientres.Header.Set("Content-Type", ct)
+	}
+
 	if attr, ok := content.Attributes["headers"]; ok {
 		val, err := attr.Expr.Value(hclCtx)
 		if err != nil {
@@ -196,14 +205,8 @@ func (e *Endpoint) newResponse(req *http.Request, evalCtx *eval.Context) (*http.
 		eval.SetHeader(val, clientres.Header)
 	}
 
-	if attr, ok := content.Attributes["body"]; ok {
-		val, err := attr.Expr.Value(hclCtx)
-		if err != nil {
-			e.log.Errorf("endpoint eval error: %v", err)
-			return nil, err
-		}
-
-		r := strings.NewReader(seetie.ValueToString(val))
+	if body != "" {
+		r := strings.NewReader(body)
 		clientres.Body = io.NopCloser(r)
 	}
 
diff --git a/handler/producer/request.go b/handler/producer/request.go
index 99d774bb..5bf56f6b 100644
--- a/handler/producer/request.go
+++ b/handler/producer/request.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/hashicorp/hcl/v2"
 
+	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/eval"
 )
@@ -24,7 +25,7 @@ type Request struct {
 // Requests represents the producer <Requests> object.
 type Requests []*Request
 
-func (r Requests) Produce(ctx context.Context, _ *http.Request, results chan<- *Result) {
+func (r Requests) Produce(ctx context.Context, req *http.Request, results chan<- *Result) {
 	var currentName string // at least pre roundtrip
 	wg := &sync.WaitGroup{}
 
@@ -40,22 +41,31 @@ func (r Requests) Produce(ctx context.Context, _ *http.Request, results chan<- *
 		}
 	}()
 
+	evalctx := ctx.Value(eval.ContextType).(*eval.Context)
+	updated := evalctx.WithClientRequest(req)
+
 	for _, or := range r {
 		outCtx := withRoundTripName(ctx, or.Name)
 
-		method, err := eval.GetContextAttribute(or.Context, outCtx, "method")
+		content, _, diags := or.Context.PartialContent(config.Request{Remain: or.Context}.Schema(true))
+		if diags.HasErrors() {
+			sendResult(ctx, results, &Result{Err: diags})
+			continue
+		}
+
+		method, err := eval.GetAttribute(updated.HCLContext(), content, "method")
 		if err != nil {
 			sendResult(ctx, results, &Result{Err: err})
 			continue
 		}
 
-		body, err := eval.GetContextAttribute(or.Context, outCtx, "body")
+		body, defaultContentType, err := eval.GetBody(updated.HCLContext(), content)
 		if err != nil {
 			sendResult(ctx, results, &Result{Err: err})
 			continue
 		}
 
-		url, err := eval.GetContextAttribute(or.Context, outCtx, "url")
+		url, err := eval.GetAttribute(updated.HCLContext(), content, "url")
 		if err != nil {
 			sendResult(ctx, results, &Result{Err: err})
 			continue
@@ -81,6 +91,10 @@ func (r Requests) Produce(ctx context.Context, _ *http.Request, results chan<- *
 			continue
 		}
 
+		if defaultContentType != "" {
+			outreq.Header.Set("Content-Type", defaultContentType)
+		}
+
 		*outreq = *outreq.WithContext(outCtx)
 		err = eval.ApplyRequestContext(outCtx, or.Context, outreq)
 		if err != nil {
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index ee9f8208..fb36b3ac 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -166,6 +166,10 @@ func CookiesToMapValue(cookies []*http.Cookie) cty.Value {
 
 func ValueToStringSlice(src cty.Value) []string {
 	var l []string
+	if !src.IsKnown() {
+		return l
+	}
+
 	switch src.Type() {
 	case cty.NilType:
 		return l
diff --git a/server/http.go b/server/http.go
index 4d6fa23f..b549412a 100644
--- a/server/http.go
+++ b/server/http.go
@@ -199,14 +199,14 @@ func (s *HTTPServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	)
 	rw = w
 
-	clientReq := req.Clone(ctx)
+	clientReq := req.Clone(req.Context())
 
 	if err := s.setGetBody(h, clientReq); err != nil {
 		mux.opts.ServerOptions.ServerErrTpl.ServeError(err).ServeHTTP(rw, req)
 		return
 	}
 
-	ctx = s.evalCtx.WithClientRequest(req)
+	ctx = s.evalCtx.WithClientRequest(clientReq)
 	*clientReq = *clientReq.WithContext(ctx)
 
 	s.accessLog.ServeHTTP(rw, clientReq, h, startTime)


Test patch:
diff --git a/eval/context_test.go b/eval/context_test.go
index 3aa13321..e5e93172 100644
--- a/eval/context_test.go
+++ b/eval/context_test.go
@@ -44,7 +44,7 @@ func TestNewHTTPContext(t *testing.T) {
 		want      http.Header
 	}{
 		{"Variables / POST", http.MethodPost, http.Header{"Content-Type": {"application/x-www-form-urlencoded"}}, bytes.NewBufferString(`user=hans`), "", baseCtx, `
-					post = req.post.user[0]
+					post = req.form_body.user[0]
 					method = req.method
 		`, http.Header{"post": {"hans"}, "method": {http.MethodPost}}},
 		{"Variables / Query", http.MethodGet, http.Header{"User-Agent": {"test/v1"}}, nil, "?name=peter", baseCtx, `
diff --git a/handler/endpoint_test.go b/handler/endpoint_test.go
index e212d1a4..d3351513 100644
--- a/handler/endpoint_test.go
+++ b/handler/endpoint_test.go
@@ -57,13 +57,13 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 		set_response_headers = {
 			X-Method = req.method
 		}`, http.MethodGet, nil, header{"X-Method": http.MethodGet}},
-		{"POST use req.post", `
+		{"POST use req.form_body", `
 		set_response_headers = {
 			X-Method = req.method
-			X-Post = req.post.foo
+			X-Form_Body = req.form_body.foo
 		}`, http.MethodPost, strings.NewReader(`foo=bar`), header{
-			"X-Method": http.MethodPost,
-			"X-Post":   "bar",
+			"X-Method":    http.MethodPost,
+			"X-Form_Body": "bar",
 		}},
 	}
 
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 54489d77..12ab7d6a 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -1093,6 +1093,7 @@ func TestHTTPServer_DynamicRequest(t *testing.T) {
 			},
 			Headers: http.Header{
 				"Content-Length": []string{"4"},
+				"Content-Type":   []string{"text/plain"},
 				"Test":           []string{"header"},
 			},
 		}},
@@ -1130,6 +1131,415 @@ func TestHTTPServer_DynamicRequest(t *testing.T) {
 	}
 }
 
+func TestHTTPServer_request_bodies(t *testing.T) {
+	client := newClient()
+
+	configFile := "testdata/integration/endpoint_eval/14_couper.hcl"
+
+	type expectation struct {
+		Body    string
+		Args    url.Values
+		Headers http.Header
+		Method  string
+	}
+
+	type testCase struct {
+		path              string
+		clientPayload     string
+		clientContentType string
+		exp               expectation
+	}
+
+	for _, tc := range []testCase{
+		{
+			"/request/body",
+			"",
+			"",
+			expectation{
+				Body:   "foo",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"3"},
+					"Content-Type":   []string{"text/plain"},
+				},
+			},
+		},
+		{
+			"/request/body/ct",
+			"",
+			"",
+			expectation{
+				Body:   "foo",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"3"},
+					"Content-Type":   []string{"application/foo"},
+				},
+			},
+		},
+		{
+			"/request/json_body/null",
+			"",
+			"",
+			expectation{
+				Body:   "null",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"4"},
+					"Content-Type":   []string{"application/json"},
+				},
+			},
+		},
+		{
+			"/request/json_body/boolean",
+			"",
+			"",
+			expectation{
+				Body:   "true",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"4"},
+					"Content-Type":   []string{"application/json"},
+				},
+			},
+		},
+		{
+			"/request/json_body/boolean/ct",
+			"",
+			"",
+			expectation{
+				Body:   "true",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"4"},
+					"Content-Type":   []string{"application/foo+json"},
+				},
+			},
+		},
+		{
+			"/request/json_body/number",
+			"",
+			"",
+			expectation{
+				Body:   "1.2",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"3"},
+					"Content-Type":   []string{"application/json"},
+				},
+			},
+		},
+		{
+			"/request/json_body/string",
+			"",
+			"",
+			expectation{
+				Body:   "\"foo\"",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"5"},
+					"Content-Type":   []string{"application/json"},
+				},
+			},
+		},
+		{
+			"/request/json_body/object",
+			"",
+			"",
+			expectation{
+				Body:   "{\"foo\":\"bar\"}",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"13"},
+					"Content-Type":   []string{"application/json"},
+				},
+			},
+		},
+		{
+			"/request/json_body/array",
+			"",
+			"",
+			expectation{
+				Body:   "[0,1,2]",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"7"},
+					"Content-Type":   []string{"application/json"},
+				},
+			},
+		},
+		/*
+			{
+				"/request/json_body/dyn",
+				"true",
+				"application/json",
+				expectation{
+					Body:   "true", // currently: "{}"
+					Args:   url.Values{},
+					Method: "POST",
+					Headers: http.Header{
+						"Content-Length": []string{"4"}, // currently: "2"
+						"Content-Type":   []string{"application/json"},
+					},
+				},
+			},
+			{
+				"/request/json_body/dyn",
+				"1.23",
+				"application/json",
+				expectation{
+					Body:   "1.23", // currently: "{}"
+					Args:   url.Values{},
+					Method: "POST",
+					Headers: http.Header{
+						"Content-Length": []string{"4"}, // currently: "2"
+						"Content-Type":   []string{"application/json"},
+					},
+				},
+			},
+			{
+				"/request/json_body/dyn",
+				"\"ab\"",
+				"application/json",
+				expectation{
+					Body:   "\"ab\"", // currently: "{}"
+					Args:   url.Values{},
+					Method: "POST",
+					Headers: http.Header{
+						"Content-Length": []string{"4"}, // currently: "2"
+						"Content-Type":   []string{"application/json"},
+					},
+				},
+			},
+		*/
+		{
+			"/request/json_body/dyn",
+			"{\"a\":3}",
+			"application/json",
+			expectation{
+				Body:   "{\"a\":3}",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"7"},
+					"Content-Type":   []string{"application/json"},
+				},
+			},
+		},
+		/*
+			{
+				"/request/json_body/dyn",
+				"[0,1]",
+				"application/json",
+				expectation{
+					Body:   "[0,1]", // currently: "{}"
+					Args:   url.Values{},
+					Method: "POST",
+					Headers: http.Header{
+						"Content-Length": []string{"5"}, // currently: "2"
+						"Content-Type":   []string{"application/json"},
+					},
+				},
+			},
+		*/
+		{
+			"/request/form_body",
+			"",
+			"",
+			expectation{
+				Body: "",
+				Args: url.Values{
+					"foo": []string{"ab c"},
+					"bar": []string{",:/"},
+				},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"22"},
+					"Content-Type":   []string{"application/x-www-form-urlencoded"},
+				},
+			},
+		},
+		{
+			"/request/form_body/ct",
+			"",
+			"",
+			expectation{
+				Body:   "bar=%2C%3A%2F&foo=ab+c",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"22"},
+					"Content-Type":   []string{"application/my-form-urlencoded"},
+				},
+			},
+		},
+		{
+			"/request/form_body/dyn",
+			"bar=%2C&foo=a",
+			"application/x-www-form-urlencoded",
+			expectation{
+				Body: "",
+				Args: url.Values{
+					"foo": []string{"a"},
+					"bar": []string{","},
+				},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"13"},
+					"Content-Type":   []string{"application/x-www-form-urlencoded"},
+				},
+			},
+		},
+	} {
+		t.Run(tc.path, func(subT *testing.T) {
+			helper := test.New(subT)
+
+			shutdown, _ := newCouper(configFile, helper)
+			defer shutdown()
+
+			req, err := http.NewRequest(http.MethodPost, "http://example.com:8080"+tc.path, strings.NewReader(tc.clientPayload))
+			helper.Must(err)
+
+			if tc.clientContentType != "" {
+				req.Header.Set("Content-Type", tc.clientContentType)
+			}
+
+			res, err := client.Do(req)
+			helper.Must(err)
+
+			resBytes, err := ioutil.ReadAll(res.Body)
+			helper.Must(err)
+			res.Body.Close()
+
+			var jsonResult expectation
+			err = json.Unmarshal(resBytes, &jsonResult)
+			if err != nil {
+				t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
+			}
+
+			if !reflect.DeepEqual(jsonResult, tc.exp) {
+				t.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult)
+			}
+		})
+	}
+}
+
+func TestHTTPServer_response_bodies(t *testing.T) {
+	client := newClient()
+
+	configFile := "testdata/integration/endpoint_eval/14_couper.hcl"
+
+	type expectation struct {
+		Body        string
+		ContentType string
+	}
+
+	type testCase struct {
+		path string
+		exp  expectation
+	}
+
+	for _, tc := range []testCase{
+		{
+			"/response/body",
+			expectation{
+				Body:        "foo",
+				ContentType: "text/plain",
+			},
+		},
+		{
+			"/response/body/ct",
+			expectation{
+				Body:        "foo",
+				ContentType: "application/foo",
+			},
+		},
+		{
+			"/response/json_body/null",
+			expectation{
+				Body:        "null",
+				ContentType: "application/json",
+			},
+		},
+		{
+			"/response/json_body/boolean",
+			expectation{
+				Body:        "true",
+				ContentType: "application/json",
+			},
+		},
+		{
+			"/response/json_body/boolean/ct",
+			expectation{
+				Body:        "true",
+				ContentType: "application/foo+json",
+			},
+		},
+		{
+			"/response/json_body/number",
+			expectation{
+				Body:        "1.2",
+				ContentType: "application/json",
+			},
+		},
+		{
+			"/response/json_body/string",
+			expectation{
+				Body:        "\"foo\"",
+				ContentType: "application/json",
+			},
+		},
+		{
+			"/response/json_body/object",
+			expectation{
+				Body:        "{\"foo\":\"bar\"}",
+				ContentType: "application/json",
+			},
+		},
+		{
+			"/response/json_body/array",
+			expectation{
+				Body:        "[0,1,2]",
+				ContentType: "application/json",
+			},
+		},
+	} {
+		t.Run(tc.path, func(subT *testing.T) {
+			helper := test.New(subT)
+
+			shutdown, _ := newCouper(configFile, helper)
+			defer shutdown()
+
+			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
+			helper.Must(err)
+
+			res, err := client.Do(req)
+			helper.Must(err)
+
+			resBytes, err := ioutil.ReadAll(res.Body)
+			helper.Must(err)
+			res.Body.Close()
+
+			if string(resBytes) != tc.exp.Body {
+				t.Errorf("%s: want: %s, got:%s", tc.path, tc.exp.Body, string(resBytes))
+			}
+
+			if ct := res.Header.Get("Content-Type"); ct != tc.exp.ContentType {
+				t.Errorf("%s: want: %s, got:%s", tc.path, tc.exp.ContentType, ct)
+			}
+		})
+	}
+}
+
 func TestHTTPServer_Endpoint_Evaluation(t *testing.T) {
 	client := newClient()
 
@@ -1187,6 +1597,114 @@ func TestHTTPServer_Endpoint_Evaluation(t *testing.T) {
 	}
 }
 
+func TestHTTPServer_Endpoint_Response_FormQuery_Evaluation(t *testing.T) {
+	client := newClient()
+
+	confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl")
+	shutdown, _ := newCouper(confPath, test.New(t))
+	defer shutdown()
+
+	helper := test.New(t)
+
+	req, err := http.NewRequest(http.MethodPost, "http://example.com:8080/req?foo=bar", strings.NewReader("s=abc123"))
+	helper.Must(err)
+	req.Header.Set("User-Agent", "")
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	res, err := client.Do(req)
+	helper.Must(err)
+
+	resBytes, err := ioutil.ReadAll(res.Body)
+	helper.Must(err)
+
+	_ = res.Body.Close()
+
+	type Expectation struct {
+		FormBody url.Values  `json:"form_body"`
+		Headers  test.Header `json:"headers"`
+		Method   string      `json:"method"`
+		Query    url.Values  `json:"query"`
+		Url      string      `json:"url"`
+	}
+
+	var jsonResult Expectation
+	err = json.Unmarshal(resBytes, &jsonResult)
+	if err != nil {
+		t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
+	}
+	exp := Expectation{
+		Method: http.MethodPost,
+		FormBody: map[string][]string{
+			"s": {"abc123"},
+		},
+		Headers: map[string]string{
+			"content-length": "8",
+			"content-type":   "application/x-www-form-urlencoded",
+		},
+		Query: map[string][]string{
+			"foo": {"bar"},
+		},
+		Url: "/req",
+	}
+	if !reflect.DeepEqual(jsonResult, exp) {
+		t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes))
+	}
+}
+
+func TestHTTPServer_Endpoint_Response_JSONBody_Evaluation(t *testing.T) {
+	client := newClient()
+
+	confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl")
+	shutdown, _ := newCouper(confPath, test.New(t))
+	defer shutdown()
+
+	helper := test.New(t)
+
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/req?foo=bar", strings.NewReader(`{"data": true}`))
+	helper.Must(err)
+	req.Header.Set("User-Agent", "")
+	req.Header.Set("Content-Type", "application/json")
+
+	res, err := client.Do(req)
+	helper.Must(err)
+
+	resBytes, err := ioutil.ReadAll(res.Body)
+	helper.Must(err)
+
+	_ = res.Body.Close()
+
+	type Expectation struct {
+		JSONBody map[string]interface{} `json:"json_body"`
+		Headers  test.Header            `json:"headers"`
+		Method   string                 `json:"method"`
+		Query    url.Values             `json:"query"`
+		Url      string                 `json:"url"`
+	}
+
+	var jsonResult Expectation
+	err = json.Unmarshal(resBytes, &jsonResult)
+	if err != nil {
+		t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
+	}
+	exp := Expectation{
+		Method: http.MethodGet,
+		JSONBody: map[string]interface{}{
+			"data": true,
+		},
+		Headers: map[string]string{
+			"content-length": "14",
+			"content-type":   "application/json",
+		},
+		Query: map[string][]string{
+			"foo": {"bar"},
+		},
+		Url: "/req",
+	}
+	if !reflect.DeepEqual(jsonResult, exp) {
+		t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes))
+	}
+}
+
 func TestHTTPServer_Endpoint_Evaluation_Inheritance(t *testing.T) {
 	client := newClient()
 
diff --git a/server/testdata/integration/endpoint_eval/14_couper.hcl b/server/testdata/integration/endpoint_eval/14_couper.hcl
new file mode 100644
index 00000000..112af5ce
--- /dev/null
+++ b/server/testdata/integration/endpoint_eval/14_couper.hcl
@@ -0,0 +1,200 @@
+server "bodies" {
+  endpoint "/request/body" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      body = "foo"
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/body/ct" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      headers = {
+        content-type = "application/foo"
+      }
+      body = "foo"
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/json_body/null" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      json_body = null
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/json_body/boolean" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      json_body = true
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/json_body/boolean/ct" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      headers = {
+        content-type = "application/foo+json"
+      }
+      json_body = true
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/json_body/number" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      json_body = 1.2
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/json_body/string" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      json_body = "foo"
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+
+  endpoint "/request/json_body/object" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      json_body = {"foo": "bar"}
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/json_body/array" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      json_body = [0,1,2]
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/json_body/dyn" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      json_body = req.json_body
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/form_body" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      form_body = {
+        foo = "ab c"
+		bar = ",:/"
+      }
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/form_body/ct" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      headers = {
+        content-type = "application/my-form-urlencoded"
+      }
+      form_body = {
+        foo = "ab c"
+		bar = ",:/"
+      }
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+  endpoint "/request/form_body/dyn" {
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+      form_body = req.form_body
+
+      backend {
+        origin = env.COUPER_TEST_BACKEND_ADDR
+      }
+    }
+  }
+
+  endpoint "/response/body" {
+    response {
+      body = "foo"
+    }
+  }
+  endpoint "/response/body/ct" {
+    response {
+      headers = {
+        content-type = "application/foo"
+      }
+      body = "foo"
+    }
+  }
+  endpoint "/response/json_body/null" {
+    response {
+      json_body = null
+    }
+  }
+  endpoint "/response/json_body/boolean" {
+    response {
+      json_body = true
+    }
+  }
+  endpoint "/response/json_body/boolean/ct" {
+    response {
+      headers = {
+        content-type = "application/foo+json"
+      }
+      json_body = true
+    }
+  }
+  endpoint "/response/json_body/number" {
+    response {
+      json_body = 1.2
+    }
+  }
+  endpoint "/response/json_body/string" {
+    response {
+      json_body = "foo"
+    }
+  }
+  endpoint "/response/json_body/object" {
+    response {
+      json_body = {"foo": "bar"}
+    }
+  }
+  endpoint "/response/json_body/array" {
+    response {
+      json_body = [0,1,2]
+    }
+  }
+}
diff --git a/server/testdata/integration/endpoint_eval/15_couper.hcl b/server/testdata/integration/endpoint_eval/15_couper.hcl
new file mode 100644
index 00000000..9a8d887a
--- /dev/null
+++ b/server/testdata/integration/endpoint_eval/15_couper.hcl
@@ -0,0 +1,8 @@
+server "bodies" {
+  endpoint "/req" {
+    response {
+      status = 200
+      json_body = req
+    }
+  }
+}