程序员社区

【Go Web开发】管理错误请求

继续上一篇的内容,当请求处理函数createMovieHandler接收到客户端发送的JSON请求体时,它可以正常工作。但在这一点上你可能会想:

  • 如果客户端发送的不是JSON,比如XML或一些随机字节,该怎么办?
  • 如果JSON格式不正确或包含错误会发生什么?
  • 如果JSON类型与我们试图解码的类型不匹配怎么办?
  • 如果请求甚至不包含body呢,又该怎么处理?

下面我们就来看看!

# 发送XML内容作为请求体
$ curl -d '<?xml version="1.0" encoding="UTF-8"?><note><to>Alice</to></note>' localhost:4000/v1/movies
{
    "error": "invalid character '\u003c' looking for beginning of value"
}

# 发送一些格式错误的JSON(注意末尾的逗号)
$ curl -d '{"title": "Moana", }' localhost:4000/v1/movies
{
    "error": "invalid character '}' looking for beginning of object key string"
}

# 送一个JSON数组而不是一个对象
$ curl -d '["foo", "bar"]' localhost:4000/v1/movies
{
    "error": "json: cannot unmarshal array into Go value of type struct { Title string    \"json:\\\"title\\\"\"; Year int32 \"json:\\\"year\\\"\"; Runtime int32 \"json:\\ \"runtime\\\"\"; Genres []string \"json:\\\"genres\\\"\" }"
}

# 发送一个数值'title'值(而不是字符串)
$ curl -d '{"title": 123}' localhost:4000/v1/movies
{
    "error": "json: cannot unmarshal number into Go struct field .title of type string"
}

# 发送一个空的请求体
$ curl -X POST localhost:4000/v1/movies
{
    "error": "EOF"
}

所有这些情况,我们可以看到createMovieHandler都在按正确请求处理。当它接收到一个无法解码到我们的input结构体中的无效请求时,不会做进一步的处理,客户端会收到一个JSON响应,其中包含Decode()方法返回的错误消息。

对于一个私有API服务的话,它不提供给公众使用,那么这种返回没什么影响,你不需要做任何其他的事情。

但是对于面向公众的API服务,错误消息本身并不理想。有些过于详细,显示了有关底层API实现的信息。其他错误信息没有足够的描述性(如“EOF”),还有一些难以理解的错误。使用的格式或语言也不一致。

为了改进这一点,我们将解释如何对Decode()返回的错误进行分类,并将它们替换为更清晰、易于操作的错误消息,以帮助客户端准确地调试JSON的错误。

对解码错误进行分类

此时,在我们的应用程序构建中,Decode()方法可能会返回以下五种类型的错误:

错误类型 原因
json.SyntaxError 正在解码的JSON存在语法问题。
io.ErrUnexpectedEOF 正在解码的JSON存在语法问题。
json.InvalidUnmarshalError 解码目标无效(通常是因为它不是指针)。这实际上是我们应用程序代码的问题,而不是JSON本身的问题。
io.EOF 正在解码的JSON是空的。

对这些潜在错误进行分类(我们可以使用Go的errors.is()和errors.as()函数来完成)将使createMovieHandler中的代码更长更复杂。这个逻辑我们也需要在整个项目的其他处理程序中重复处理。

所以,我们在cmd/api/helpers.go中创建一个新的readJSON()帮助函数。将从请求体中正常解码JSON,然后对错误进行分类,并在必要时用我们自己的定制消息替换它们。

如果你跟随本系列文章来操作,请继续并将以下代码添加到cmd/api/helper.go文件:

File: cmd/api/helpers.go


package main

import (
    "encoding/json"
    "errrors"
    "fmt"
    "io"
    "net/http"
    "strconv"

    "github.com/julienschmidt/httprouter"
)

func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
    //将请求body解析到dst中
    err := json.NewDecoder(r.Body).Decode(dst)
    if err != nil {
        //如果在解析的过程中发生错误,开始分类...
        var syntaxError *json.SyntaxError
        var unmarshalTypeError * json.UnmarshalTypeError
        var invalidUnmarshalError *json.InvalidUnmarshalError

        switch {
        //使用errors.As()函数检查错误类型是否为*json.SyntaxError。如果是,返回错误
        case errors.As(err, &syntaxError):
            return fmt.Errorf("body contain badly-formed JSON (at charcter %d)", syntaxErr.Offset)
        //某些情况下,因为语法错误Decode()函数可能返回io.ErrUnexpectedEOF错误。
        case errors.Is(err, io.ErrUnexpectedEOF):
            return errors.New("body contain badly-formed JSON")
        //同样捕获*json.UnmarshalTypeError错误,这些错误是因为JSON值和接收对象不匹配。如果错误对应到特定到字段,
        //我们可以指出哪个字段错误方便客户端debug
        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 contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
         //如果解码到内容是空对象,会返回io.EOF。
         case errors.Is(err, io.EOF):
             return errors.New("body must not be empty")
          //如果decode()函数传入一个非空的指针,将返回json.InvalidUnmarshalError。
          case errors.As(err, &invalidUnmarshalError):
              panic(err)
          default:
              return err
        }
    }
    return nil
}

有了这个新的帮助函数,让我们回到cmd/api/movies.go文件并更新createMovieHandler来使用它。像这样:

File: cmd/api/movies.go


package main

import (
    "fmt"
    "net/http"
    "time"

    "greenlight.alexedwards.net/internal/data"
)

func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title      string   `json:"title"`
        Year       int32    `json:"year"`
        Runtime    int32    `json:"runtime"`
        Genres     []string `json:"genres"`
    }
    //使用新的readJSON()帮助函数解析请求体到input结构体实例。如果返回错误,我们将错误信息携带400错误码返回客户端
    err := app.readJSON(w, r, &input)
    if err != nil {
        app.errorResponse(w, r, http.StatusBadRequest, err.Error())
        return
    }
    fmt.Fprintf(w, "%+v\n", input)
}

...

重新启动API,然后通过重复我们在本节开始时发出的那些相同的错误请求。现在,你应该看到我们新的、自定义的错误消息,类似如下:

# 发送一些XML作为请求体
$ curl -d '<?xml version="1.0" encoding="UTF-8"?><note><to>Alice</to></note>' localhost:4000/v1/movies
{
    "error": "body contains badly-formed JSON (at character 1)"
}

# 发送一些格式错误的JSON(注意末尾的逗号)
$ curl -d '{"title": "Moana", }' localhost:4000/v1/movies
{
    "error": "body contains badly-formed JSON (at character 20)"
}

# 送一个JSON数组而不是一个对象
$ curl -d '["foo", "bar"]' localhost:4000/v1/movies
{
    "error": "body contains incorrect JSON type (at character 1)"
}

# 发送一个数值'title'值(而不是字符串)
$ curl -d '{"title": 123}' localhost:4000/v1/movies
{
    "error": "body contains incorrect JSON type for \"title\""
}

# 发送一个空的请求体
$ curl -X POST localhost:4000/v1/movies
{
    "error": "body must not be empty"
}

它们看起来真不错。错误消息现在在格式上更简单、更清晰和一致,而且它们不会暴露任何有关底层程序的不必要信息。

如果您愿意,可以随意使用它,并尝试发送不同的请求主体,以查看处理程序如何响应。

创建错误请求处理函数

在上面的createMovieHandler代码中,我们使用通用的app.errorResponse()帮助函数向客户端发送一个400 Bad Request响应和错误消息。

我们用一个专业的app.badRequestResponse()帮助函数代替它:

File: cmd/api/errors.go


package main

...

func (app *application) bandRequestResponse(w http.ResponseWriter, r *http.Request) {
    app.errorResponse(w, r, http.StatusBadReqeust, err.Error())
}

File: cmd/api/movies.go


package main

...

func(app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title      string   `json:"title"`
        Year       int32    `json:"year"`
        Runtime    int32    `json:"runtime"`
        Genres     []string `json:"genres"`
    }

    err := app.readJSON(w, r, &input)
    if err != nil {
        //使用新的badRequestResponse()帮助函数
        app.badRequestResponse(w, r, err)
        return 
    }
    fmt.Fprintf(w, "%+v\n", input)
}

这是一个很小的修改,但很有用。随着我们的应用程序变得越来越复杂,使用像这样的专业帮助函数来管理不同类型的错误将有助于确保我们的错误响应在所有接口上保持一致。

附加内容

panic对比返回错误

在之前错误分类中,碰到json.InvalidUnmarshalError错误时,我们在readJSON函数中直接panic。你可能觉得在go中错误一般都向上返回给调用者。但是在一些特殊情况下panic是没问题的。所以在必要都时候不需要坚持不panic的教条。当应用程序发生错误时,这里区分两类不同的错误是有用的。

第一类错误是在正常的操作过程中能预期到会发生的。预期错误的一些例子包括:数据库查询超时、网络资源不可用或用户输入错误。这些错误并不一定意味着您的程序本身有问题——事实上,它们通常是由程序控制之外的事情引起的。任何时候,返回这类错误并优雅地处理它们都是一种良好的实践。

另一类错误是非预期的错误。这些错误在正常操作中不应该发生,如果它们发生了,可能是开发人员错误或代码库中的逻辑错误造成的。这些错误确实是异常的,在这种情况下使用panic更被广泛接受。事实上,Go标准库经常会这么处理,当发生逻辑错误或试图以非预期的方式使用语言特性时——比如试图访问切片中的越界索引,或试图关闭已经关闭的通道。

但即便如此,我还是建议在大多数情况下尝试返回并优雅地处理意外错误。特殊情况是当返回错误时,会给代码库的其余部分增加不可接受的错误处理工作量。

回到readJSON()帮助函数,如果运行时发生json.InvalidUnmarshalError,因为开发人员给Decode()函数传递错误的值导致。这绝对是一个在正常操作下不应该看到的意外错误,而且应该在部署之前在开发和测试中发现。

如果我们返回错误而不是panic,我们需要在每个api处理程序中引入额外的代码来管理这些错误。对于不太可能在生产环境中看到的错误,这似乎不是一个很好的权衡。

这篇文章很好地总结了panic。

panic通常是指发生了非可预期错误。大多数情况下,正常操作中不应该发生的错误,我们使用它来快速失败,因为没有准备好优雅地处理这类错误。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 【Go Web开发】管理错误请求

一个分享Java & Python知识的社区