diff --git a/js/modules/k6/http/response_test.go b/js/modules/k6/http/response_test.go index 0024310682a2..4c77562df81d 100644 --- a/js/modules/k6/http/response_test.go +++ b/js/modules/k6/http/response_test.go @@ -80,6 +80,11 @@ const jsonData = `{"glossary": { "GlossSeeAlso": ["GML","XML"]}, "GlossSee": "markup"}}}}}` +const invalidJSONData = `{ + "a":"apple", + "t":testing" +}` + func myFormHandler(w http.ResponseWriter, r *http.Request) { var body []byte var err error @@ -110,6 +115,14 @@ func jsonHandler(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(body) } +func invalidJSONHandler(w http.ResponseWriter, r *http.Request) { + body := []byte(invalidJSONData) + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) + w.WriteHeader(200) + _, _ = w.Write(body) +} + func TestResponse(t *testing.T) { tb, state, samples, rt, _ := newRuntime(t) defer tb.Cleanup() @@ -118,6 +131,7 @@ func TestResponse(t *testing.T) { tb.Mux.HandleFunc("/myforms/get", myFormHandler) tb.Mux.HandleFunc("/json", jsonHandler) + tb.Mux.HandleFunc("/invalidjson", invalidJSONHandler) t.Run("Html", func(t *testing.T) { _, err := common.RunString(rt, sr(` @@ -178,7 +192,14 @@ func TestResponse(t *testing.T) { t.Run("Invalid", func(t *testing.T) { _, err := common.RunString(rt, sr(`http.request("GET", "HTTPBIN_URL/html").json();`)) - assert.EqualError(t, err, "GoError: invalid character '<' looking for beginning of value") + //nolint:lll + assert.EqualError(t, err, "GoError: cannot parse json due to an error at line 1, character 2 , error: invalid character '<' looking for beginning of value") + }) + + t.Run("Invalid", func(t *testing.T) { + _, err := common.RunString(rt, sr(`http.request("GET", "HTTPBIN_URL/invalidjson").json();`)) + //nolint:lll + assert.EqualError(t, err, "GoError: cannot parse json due to an error at line 3, character 9 , error: invalid character 'e' in literal true (expecting 'r')") }) }) t.Run("JsonSelector", func(t *testing.T) { diff --git a/lib/netext/httpext/response.go b/lib/netext/httpext/response.go index c9151b2d4597..9a3d3f002568 100644 --- a/lib/netext/httpext/response.go +++ b/lib/netext/httpext/response.go @@ -24,6 +24,9 @@ import ( "context" "crypto/tls" "encoding/json" + "fmt" + "net/http" + "net/http/httputil" "github.com/loadimpact/k6/lib/netext" "github.com/pkg/errors" @@ -55,6 +58,17 @@ const ( ResponseTypeNone ) +type jsonError struct { + line int + character int + err error +} + +func (j jsonError) Error() string { + errMessage := "cannot parse json due to an error at line" + return fmt.Sprintf("%s %d, character %d , error: %v", errMessage, j.line, j.character, j.err) +} + // ResponseTimings is a struct to put all timings for a given HTTP response/request type ResponseTimings struct { Duration float64 `json:"duration"` @@ -127,7 +141,6 @@ func (res *Response) JSON(selector ...string) (interface{}, error) { } if hasSelector { - if !res.validatedJSON { if !gjson.ValidBytes(body) { return nil, nil @@ -144,11 +157,35 @@ func (res *Response) JSON(selector ...string) (interface{}, error) { } if err := json.Unmarshal(body, &v); err != nil { + if syntaxError, ok := err.(*json.SyntaxError); ok { + err = checkErrorInJSON(body, int(syntaxError.Offset), err) + } return nil, err } res.validatedJSON = true res.cachedJSON = v } return res.cachedJSON, nil +} + +func checkErrorInJSON(input []byte, offset int, err error) error { + lf := '\n' + str := string(input) + + // Humans tend to count from 1. + line := 1 + character := 0 + + for i, b := range str { + if b == lf { + line++ + character = 0 + } + character++ + if i == offset { + break + } + } + return jsonError{line: line, character: character, err: err} }