在上一篇文章中为处理HTTP请求中的无效JSON内容和其他错误请求所做的优化是朝着正确方向迈出了一大步。但我们仍然可以做一些事情来使JSON处理更加健壮。
其中的一点就是处理HTTP请求中的未知字段。例如,下面的shell命令,向createMovieHandler接口发送一个包含未知字段rating的请求:
$ curl -i -d '{"title": "Moana", "rating":"PG"}' localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 18:51:50 GMT Content-Length: 41
Content-Type: text/plain; charset=utf-8
{Title:Moana Year:0 Runtime:0 Genres:[]}
请注意这个请求是如何正常地工作的——没有通知客户端,应用程序碰到无法识别的rating字段。在某些场景中,默默忽略未知字段可能正是您想要的行为,但在这里,最好能够提醒客户端注意这个问题。
幸运的是json.Decoder提供了DisallowUnknownField()配制可以在碰到未知字段时报错。
另一个问题是json.Decoder支持JSON数据流。当我们在请求体上调用Decode()时,它实际上只从请求体读取第一个JSON值并解码。如果我们对Decode()进行第二次调用,它将读取并解码第二个JSON值,以此类推。
但是因为我们在readJSON()中只调用了一次Decode(),所以在请求体中第一个JSON值之后的任何内容都会被忽略。这意味着您可以发送一个包含多个JSON值的请求体,或者在第一个JSON值之后发送垃圾内容,而我们的API处理程序不会引发错误。例如:
# Body包含多个JSON值
$ curl -i -d '{"title": "Moana"}{"title": "Top Gun"}' localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 18:53:57 GMT Content-Length: 41
Content-Type: text/plain; charset=utf-8
{Title:Moana Year:0 Runtime:0 Genres:[]}
# Body在第一个JSON值之后包含垃圾内容
$ curl -i -d '{"title": "Moana"} :~()' localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 18:54:15 GMT Content-Length: 41
Content-Type: text/plain; charset=utf-8
{Title:Moana Year:0 Runtime:0 Genres:[]}
同样,这种行为可能非常有用,但在解析API请求时它并不适合。我们希望createMovieHandler处理程序的请求在请求体中只包含一个JSON对象,并只包含关于创建的电影的信息。
为了确保请求体中没有额外的JSON值(或任何其他内容),我们需要在readJSON()函数中第二次调用Decode(),并检查它是否返回一个io.EOF(文件结束)错误。
最后,目前我们对接受的请求体大小没有限制。这意味着对于任何企图对我们的API执行Dos攻击的恶意客户端来说,我们的createMovieHandler将是一个很脆弱的目标。我们可以通过使用http.MaxBytesReader()函数来限制请求体的最大值来解决这个问题。
让我们更新readJSON()函数来修复前面的三个问题:
File: cmd/api/helper.go
package main
...
func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
maxBytes := 1_048_576
//使用http.MaxBytesReader函数限制只读取1MB以内的请求内容
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
dec := json.NewDecoder(r.Body)
//启用DisallowUnknownFields()函数,这意味着在解析JSON的过程中,如果有不可匹配的字段就返回错误。
dec.DisallowUnknownFields()
err := dec.Decode(dst)
if err != nil {
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
var invalidUnmarshalError *json.InvalidUnmarshalError
switch {
case errors.As(err, &syntaxError):
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
case errors.Is(err, io.ErrUnexpectedEOF):
return errors.New("body contains badly-formed JSON")
case errors.As(err, &unmarshalTypeError):
if unmarshalTypeError.Field != "" {
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
}
return fmt.Errorf("body contain incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
case errors.Is(err, io.EOF):
return errors.New("body must not be empty")
//如果JSON中包含不能解析的字段,将返回“json: unknown field”错误,这里从错误内容中提取出字段名称
//返回给客户端
case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("body contain unknown key %s", fieldName)
//如果请求内容超过1MB将返回"http: request body too large"错误,返回给客户端
case err.Error() == "http: request body too large":
return fmt.Errorf("body must not be large than %d bytes", maxBytes)
case errors.As(err, &invalidUnmarshalError):
panic(err)
default:
return err
}
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("body must only contain a single JSON value")
}
return nil
}
修改完之后,我们再次尝试本章前面的请求:
$ curl -d '{"title": "Moana", "rating":"PG"}' localhost:4000/v1/movies
{
"error": "body contains unknown key \"rating\""
}
$ curl -d '{"title": "Moana"}{"title": "Top Gun"}' localhost:4000/v1/movies
{
"error": "body must only contain a single JSON value"
}
$ curl -d '{"title": "Moana"} :~()' localhost:4000/v1/movies
{
"error": "body must only contain a single JSON value"
}
根据上面的结果发现请求处理被终止,客户端收到一个明确的错误消息,解释了错误原因。
下面可以发送一个包含大数据量的请求,看看响应结果。
为了演示,作者创建了一个1.5MB的JSON文件,您可以通过运行以下命令将其下载到/tmp目录:
$ wget -O /tmp/largefile.json https://www.alexedwards.net/static/largefile.json
如果你将这个文件内容作为请求体发送到POST /v1/movies接口,http.MaxByteReader()会检查并报错,你将收到如下响应信息:
$ curl -d @/tmp/largefile.json localhost:4000/v1/movies
{
"error": "body must not be larger than 1048576 bytes"
}
完成以上修改,我们总算完成了readJSON()函数的业务处理。
必须承认readJSON()里面的代码并不是最漂亮的…我们引入了许多错误处理和逻辑,用于对Decode()函数返回错误的处理。但现在已经写好了。您不需要再修改它,而且您可以轻松地将它复制和粘贴到其他项目中。