Repository: avenga/couper (go)

Issue:
`req.json_body` result is an empty object for specific types
**Describe the bug**
The variable `req.json_body` does not return the expected outcome.

**To Reproduce**
Steps to reproduce the behavior:
1. Which Couper version? Run `couper version` or `docker run --entrypoint=/couper avenga/couper version`
`master 2021-03-29 acfce46`
2. Provide your configuration file `*.hcl`. Remove sensitive data.
```hcl
server "api" {
  api {
    base_path = "/api"

    endpoint "/req" {
      response {
#        body = json_encode(req.json_body)
        body = json_encode([1,2,3])
      }
    }
  }
}
```


3. Provide a `curl` call for reproduction
`curl -s -H "Content-Type: application/json" -d "[1,2,3]" localhost:8080/api/req|jq`

**Expected behavior**
- `json_encode([1,2,3])`: got expected
  
```json
 [
  1,
  2,
  3
]
```

- `json_encode(req.json_body)` got empty object, expected array with `1, 2, 3`

```json
 {}
```

**Additional context**
This happens with `boolean`, `number`, `string` or `array` values.


Pull request:
Fix json type assumption
Use a more generic approach and convert to explicit cty types later on

Closes #165 

Patch:
diff --git a/eval/context.go b/eval/context.go
index 4eba81de..fe35f002 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -3,7 +3,6 @@ package eval
 import (
 	"bytes"
 	"context"
-	"encoding/json"
 	"io"
 	"io/ioutil"
 	"mime"
@@ -18,6 +17,7 @@ import (
 	"github.com/zclconf/go-cty/cty"
 	"github.com/zclconf/go-cty/cty/function"
 	"github.com/zclconf/go-cty/cty/function/stdlib"
+	ctyjson "github.com/zclconf/go-cty/cty/json"
 
 	"github.com/avenga/couper/config/jwt"
 	"github.com/avenga/couper/config/request"
@@ -106,7 +106,7 @@ 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)),
+		JsonBody:  parseReqJSON(req),
 		Method:    cty.StringVal(req.Method),
 		Path:      cty.StringVal(req.URL.Path),
 		PathParam: seetie.MapToValue(pathParams),
@@ -148,13 +148,13 @@ func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
 			URL:      cty.StringVal(newRawURL(bereq.URL).String()),
 		}.Merge(newVariable(ctx.inner, bereq.Cookies(), bereq.Header)))
 
-		var jsonBody map[string]interface{}
+		var jsonBody cty.Value
 		if (ctx.bufferOption & BufferResponse) == BufferResponse {
 			jsonBody = parseRespJSON(beresp)
 		}
 		resps[name] = cty.ObjectVal(ContextMap{
 			HttpStatus: cty.StringVal(strconv.Itoa(beresp.StatusCode)),
-			JsonBody:   seetie.MapToValue(jsonBody),
+			JsonBody:   jsonBody,
 		}.Merge(newVariable(ctx.inner, beresp.Cookies(), beresp.Header)))
 	}
 
@@ -230,40 +230,52 @@ func isJSONMediaType(contentType string) bool {
 	return m == "application/json"
 }
 
-func parseJSON(r io.Reader) map[string]interface{} {
+func parseJSON(r io.Reader) cty.Value {
 	if r == nil {
-		return nil
+		return cty.NilVal
 	}
-	var result map[string]interface{}
+
 	b, err := ioutil.ReadAll(r)
 	if err != nil {
-		return nil
+		return cty.NilVal
+	}
+
+	impliedType, err := ctyjson.ImpliedType(b)
+	if err != nil {
+		return cty.NilVal
+	}
+
+	val, err := ctyjson.Unmarshal(b, impliedType)
+	if err != nil {
+		return cty.NilVal
 	}
-	_ = json.Unmarshal(b, &result)
-	return result
+	return val
 }
 
-func parseReqJSON(req *http.Request) map[string]interface{} {
+func parseReqJSON(req *http.Request) cty.Value {
 	if req.GetBody == nil {
-		return nil
+		return cty.EmptyObjectVal
 	}
 
 	if !isJSONMediaType(req.Header.Get("Content-Type")) {
-		return nil
+		return cty.EmptyObjectVal
 	}
 
 	body, _ := req.GetBody()
-	result := parseJSON(body)
-	return result
+	return parseJSON(body)
 }
 
-func parseRespJSON(beresp *http.Response) map[string]interface{} {
+func parseRespJSON(beresp *http.Response) cty.Value {
 	if !isJSONMediaType(beresp.Header.Get("Content-Type")) {
-		return nil
+		return cty.EmptyObjectVal
 	}
 
 	buf := &bytes.Buffer{}
-	io.Copy(buf, beresp.Body) // TODO: err handling
+	_, err := io.Copy(buf, beresp.Body)
+	if err != nil {
+		return cty.EmptyObjectVal
+	}
+
 	// reset
 	beresp.Body = NewReadCloser(bytes.NewBuffer(buf.Bytes()), beresp.Body)
 	return parseJSON(buf)
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index fb36b3ac..73c873ad 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -166,7 +166,7 @@ func CookiesToMapValue(cookies []*http.Cookie) cty.Value {
 
 func ValueToStringSlice(src cty.Value) []string {
 	var l []string
-	if !src.IsKnown() {
+	if !src.IsKnown() || src.IsNull() {
 		return l
 	}
 


Test patch:
diff --git a/eval/context_test.go b/eval/context_test.go
index e5e93172..33c143f3 100644
--- a/eval/context_test.go
+++ b/eval/context_test.go
@@ -79,7 +79,7 @@ func TestNewHTTPContext(t *testing.T) {
 		{"Variables / GET /w json body & null value", http.MethodGet, http.Header{"Content-Type": {"application/json"}}, bytes.NewBufferString(`{"json": null}`), "", baseCtx, `
 			method = req.method
 			title = req.json_body.json
-		`, http.Header{"method": {http.MethodGet}, "title": {""}}},
+		`, http.Header{"method": {http.MethodGet}, "title": nil}},
 	}
 
 	for _, tt := range tests {
@@ -116,7 +116,7 @@ func TestNewHTTPContext(t *testing.T) {
 
 				result := seetie.ValueToStringSlice(cv)
 				if !reflect.DeepEqual(v, result) {
-					t.Errorf("Expected %q, got: %#v", v, cv)
+					t.Errorf("Expected %q, got: %#v, type: %#v", v, result, cv)
 				}
 			}
 		})
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 12ab7d6a..6bb25cbc 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -1277,80 +1277,76 @@ func TestHTTPServer_request_bodies(t *testing.T) {
 				},
 			},
 		},
-		/*
-			{
-				"/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",
+			"true",
+			"application/json",
+			expectation{
+				Body:   "true",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"4"},
+					"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",
+			"1.23",
+			"application/json",
+			expectation{
+				Body:   "1.23",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"4"},
+					"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",
+			"\"ab\"",
+			"application/json",
+			expectation{
+				Body:   "\"ab\"",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"4"},
+					"Content-Type":   []string{"application/json"},
 				},
 			},
-		*/
+		},
 		{
 			"/request/json_body/dyn",
-			"{\"a\":3}",
+			"{\"a\":3,\"b\":[]}",
 			"application/json",
 			expectation{
-				Body:   "{\"a\":3}",
+				Body:   "{\"a\":3,\"b\":[]}",
 				Args:   url.Values{},
 				Method: "POST",
 				Headers: http.Header{
-					"Content-Length": []string{"7"},
+					"Content-Length": []string{"14"},
 					"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/json_body/dyn",
+			"[0,1]",
+			"application/json",
+			expectation{
+				Body:   "[0,1]",
+				Args:   url.Values{},
+				Method: "POST",
+				Headers: http.Header{
+					"Content-Length": []string{"5"},
+					"Content-Type":   []string{"application/json"},
 				},
 			},
-		*/
+		},
 		{
 			"/request/form_body",
 			"",
@@ -1705,6 +1701,68 @@ func TestHTTPServer_Endpoint_Response_JSONBody_Evaluation(t *testing.T) {
 	}
 }
 
+func TestHTTPServer_Endpoint_Response_JSONBody_Array_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)
+
+	content := `[1, 2, {"data": true}]`
+
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/req?foo=bar", strings.NewReader(content))
+	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 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: []interface{}{
+			1,
+			2,
+			map[string]interface{}{
+				"data": true,
+			},
+		},
+		Headers: map[string]string{
+			"content-length": strconv.Itoa(len(content)),
+			"content-type":   "application/json",
+		},
+		Query: map[string][]string{
+			"foo": {"bar"},
+		},
+		Url: "/req",
+	}
+
+	if fmt.Sprint(jsonResult) != fmt.Sprint(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()