MM-10232, MM-10259: Improve error handling from invalid json (#8668)
* MM-10232: improve error handling from malformed slash command responses Switch to json.Unmarshal, which doesn't obscure JSON parse failures like json.Decode. The latter is primarily designed for streams of JSON, not necessarily unmarshalling just a single object. * rework HumanizedJsonError to expose Line and Character discretely * MM-10259: pinpoint line and character where json config error occurs * tweak HumanizeJsonError to accept err first
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d3f09b54e2
Коммит
6d50d836f5
@@ -831,8 +831,10 @@ func (c *Client) Command(channelId string, command string) (*Result, *AppError)
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
|
||||
response, _ := CommandResponseFromJson(r.Body)
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), CommandResponseFromJson(r.Body)}, nil
|
||||
r.Header.Get(HEADER_ETAG_SERVER), response}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2991,7 +2991,9 @@ func (c *Client4) ExecuteCommand(channelId, command string) (*CommandResponse, *
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return CommandResponseFromJson(r.Body), BuildResponse(r)
|
||||
|
||||
response, _ := CommandResponseFromJson(r.Body)
|
||||
return response, BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3007,7 +3009,9 @@ func (c *Client4) ExecuteCommandWithTeam(channelId, teamId, command string) (*Co
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return CommandResponseFromJson(r.Body), BuildResponse(r)
|
||||
|
||||
response, _ := CommandResponseFromJson(r.Body)
|
||||
return response, BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/utils/jsonutils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,14 +33,14 @@ func (o *CommandResponse) ToJson() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func CommandResponseFromHTTPBody(contentType string, body io.Reader) *CommandResponse {
|
||||
func CommandResponseFromHTTPBody(contentType string, body io.Reader) (*CommandResponse, error) {
|
||||
if strings.TrimSpace(strings.Split(contentType, ";")[0]) == "application/json" {
|
||||
return CommandResponseFromJson(body)
|
||||
}
|
||||
if b, err := ioutil.ReadAll(body); err == nil {
|
||||
return CommandResponseFromPlainText(string(b))
|
||||
return CommandResponseFromPlainText(string(b)), nil
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func CommandResponseFromPlainText(text string) *CommandResponse {
|
||||
@@ -47,15 +49,19 @@ func CommandResponseFromPlainText(text string) *CommandResponse {
|
||||
}
|
||||
}
|
||||
|
||||
func CommandResponseFromJson(data io.Reader) *CommandResponse {
|
||||
decoder := json.NewDecoder(data)
|
||||
var o CommandResponse
|
||||
func CommandResponseFromJson(data io.Reader) (*CommandResponse, error) {
|
||||
b, err := ioutil.ReadAll(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := decoder.Decode(&o); err != nil {
|
||||
return nil
|
||||
var o CommandResponse
|
||||
err = json.Unmarshal(b, &o)
|
||||
if err != nil {
|
||||
return nil, jsonutils.HumanizeJsonError(err, b)
|
||||
}
|
||||
|
||||
o.Attachments = StringifySlackFieldValue(o.Attachments)
|
||||
|
||||
return &o
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
@@ -6,18 +6,10 @@ package model
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommandResponseJson(t *testing.T) {
|
||||
o := CommandResponse{Text: "test"}
|
||||
json := o.ToJson()
|
||||
ro := CommandResponseFromJson(strings.NewReader(json))
|
||||
|
||||
if o.Text != ro.Text {
|
||||
t.Fatal("Ids do not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandResponseFromHTTPBody(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
ContentType string
|
||||
@@ -29,95 +21,137 @@ func TestCommandResponseFromHTTPBody(t *testing.T) {
|
||||
{"application/json", `{"text": "foo"}`, "foo"},
|
||||
{"application/json; charset=utf-8", `{"text": "foo"}`, "foo"},
|
||||
} {
|
||||
response := CommandResponseFromHTTPBody(test.ContentType, strings.NewReader(test.Body))
|
||||
if response.Text != test.ExpectedText {
|
||||
t.Fatal()
|
||||
}
|
||||
response, err := CommandResponseFromHTTPBody(test.ContentType, strings.NewReader(test.Body))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.ExpectedText, response.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandResponseFromPlainText(t *testing.T) {
|
||||
response := CommandResponseFromPlainText("foo")
|
||||
if response.Text != "foo" {
|
||||
t.Fatal("text should be foo")
|
||||
}
|
||||
assert.Equal(t, "foo", response.Text)
|
||||
}
|
||||
|
||||
func TestCommandResponseFromJson(t *testing.T) {
|
||||
json := `{
|
||||
"response_type": "ephemeral",
|
||||
"text": "response text",
|
||||
"username": "response username",
|
||||
"icon_url": "response icon url",
|
||||
"goto_location": "response goto location",
|
||||
"attachments": [{
|
||||
"text": "attachment 1 text",
|
||||
"pretext": "attachment 1 pretext"
|
||||
},{
|
||||
"text": "attachment 2 text",
|
||||
"fields": [{
|
||||
"title": "field 1",
|
||||
"value": "value 1",
|
||||
"short": true
|
||||
},{
|
||||
"title": "field 2",
|
||||
"value": [],
|
||||
"short": false
|
||||
}]
|
||||
}]
|
||||
}`
|
||||
t.Parallel()
|
||||
|
||||
response := CommandResponseFromJson(strings.NewReader(json))
|
||||
|
||||
if response == nil {
|
||||
t.Fatal("should've received non-nil CommandResponse")
|
||||
sToP := func(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
if response.ResponseType != "ephemeral" {
|
||||
t.Fatal("should've received correct response type")
|
||||
} else if response.Text != "response text" {
|
||||
t.Fatal("should've received correct response text")
|
||||
} else if response.Username != "response username" {
|
||||
t.Fatal("should've received correct response username")
|
||||
} else if response.IconURL != "response icon url" {
|
||||
t.Fatal("should've received correct response icon url")
|
||||
} else if response.GotoLocation != "response goto location" {
|
||||
t.Fatal("should've received correct response goto location")
|
||||
testCases := []struct {
|
||||
Description string
|
||||
Json string
|
||||
ExpectedCommandResponse *CommandResponse
|
||||
ExpectedError *string
|
||||
}{
|
||||
{
|
||||
"empty response",
|
||||
"",
|
||||
nil,
|
||||
sToP("parsing error at line 1, character 1: unexpected end of JSON input"),
|
||||
},
|
||||
{
|
||||
"malformed response",
|
||||
`{"text": }`,
|
||||
nil,
|
||||
sToP("parsing error at line 1, character 11: invalid character '}' looking for beginning of value"),
|
||||
},
|
||||
{
|
||||
"invalid response",
|
||||
`{"text": "test", "response_type": 5}`,
|
||||
nil,
|
||||
sToP("parsing error at line 1, character 36: json: cannot unmarshal number into Go struct field CommandResponse.response_type of type string"),
|
||||
},
|
||||
{
|
||||
"ephemeral response",
|
||||
`{
|
||||
"response_type": "ephemeral",
|
||||
"text": "response text",
|
||||
"username": "response username",
|
||||
"icon_url": "response icon url",
|
||||
"goto_location": "response goto location",
|
||||
"attachments": [{
|
||||
"text": "attachment 1 text",
|
||||
"pretext": "attachment 1 pretext"
|
||||
},{
|
||||
"text": "attachment 2 text",
|
||||
"fields": [{
|
||||
"title": "field 1",
|
||||
"value": "value 1",
|
||||
"short": true
|
||||
},{
|
||||
"title": "field 2",
|
||||
"value": [],
|
||||
"short": false
|
||||
}]
|
||||
}]
|
||||
}`,
|
||||
&CommandResponse{
|
||||
ResponseType: "ephemeral",
|
||||
Text: "response text",
|
||||
Username: "response username",
|
||||
IconURL: "response icon url",
|
||||
GotoLocation: "response goto location",
|
||||
Attachments: []*SlackAttachment{
|
||||
{
|
||||
Text: "attachment 1 text",
|
||||
Pretext: "attachment 1 pretext",
|
||||
},
|
||||
{
|
||||
Text: "attachment 2 text",
|
||||
Fields: []*SlackAttachmentField{
|
||||
{
|
||||
Title: "field 1",
|
||||
Value: "value 1",
|
||||
Short: true,
|
||||
},
|
||||
{
|
||||
Title: "field 2",
|
||||
Value: "[]",
|
||||
Short: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"null array items",
|
||||
`{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}`,
|
||||
&CommandResponse{
|
||||
Attachments: []*SlackAttachment{
|
||||
{
|
||||
Fields: []*SlackAttachmentField{
|
||||
{
|
||||
Title: "foo",
|
||||
Value: "bar",
|
||||
Short: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
attachments := response.Attachments
|
||||
if len(attachments) != 2 {
|
||||
t.Fatal("should've received 2 attachments")
|
||||
} else if attachments[0].Text != "attachment 1 text" {
|
||||
t.Fatal("should've received correct first attachment text")
|
||||
} else if attachments[0].Pretext != "attachment 1 pretext" {
|
||||
t.Fatal("should've received correct first attachment pretext")
|
||||
} else if attachments[1].Text != "attachment 2 text" {
|
||||
t.Fatal("should've received correct second attachment text")
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fields := attachments[1].Fields
|
||||
if len(fields) != 2 {
|
||||
t.Fatal("should've received 2 fields")
|
||||
} else if fields[0].Value.(string) != "value 1" {
|
||||
t.Fatal("should've received correct first attachment value")
|
||||
} else if _, ok := fields[1].Value.(string); !ok {
|
||||
t.Fatal("should've received second attachment value parsed as a string")
|
||||
} else if fields[1].Value.(string) != "[]" {
|
||||
t.Fatal("should've received correct second attachment value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandResponseNullArrayItems(t *testing.T) {
|
||||
payload := `{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}`
|
||||
cr := CommandResponseFromJson(strings.NewReader(payload))
|
||||
if cr == nil {
|
||||
t.Fatal("CommandResponse should not be nil")
|
||||
}
|
||||
if len(cr.Attachments) != 1 {
|
||||
t.Fatalf("expected one attachment")
|
||||
}
|
||||
if len(cr.Attachments[0].Fields) != 1 {
|
||||
t.Fatalf("expected one field")
|
||||
response, err := CommandResponseFromJson(strings.NewReader(testCase.Json))
|
||||
if testCase.ExpectedError != nil {
|
||||
assert.EqualError(t, err, *testCase.ExpectedError)
|
||||
assert.Nil(t, response)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, response) {
|
||||
assert.Equal(t, testCase.ExpectedCommandResponse, response)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user