使用 Ctx.Output.Status = HTTPStatusCode 代替 Ctx.ResponseWriter.WriteHeader(HTTPStatusCode)直接设置状态码

0.运行环境

├── Bee		  : v1.10.0
├── Beego     : 1.12.0
├── GoVersion : go1.12.9
├── GOOS      : windows
├── GOARCH    : amd64
├── NumCPU    : 8
├── Compiler  : gc
└── Date      : Monday, 18 Nov 2019

1.解决方法(直接)

使用 Ctx.Output.Status = HTTPStatusCode 代替 Ctx.ResponseWriter.WriteHeader(HTTPStatusCode)直接设置状态码


2.具体问题

使用 Ctx.ResponseWriter.WriteHeader 设置Response的HTTP Status Code(状态码)后会导致Header中的 Content-Typetext/plain; charset=utf-8,影响JSON数据类型识别。


3.问题演示

// 测试代码
func (c *YourController) Get() {
	c.Ctx.ResponseWriter.WriteHeader(405)
	c.Data["json"] = data
	c.ServeJSON()
}

Postman测试截图

(可见Content-Type被强制设置为text/plain; charset=utf-8) 问题演示


4.问题分析

从源码入手,因为WriteHeaderServeJSON前面执行,所以看一下WriteHeaderServeJSON方法的做了什么。最终发现WriteHeader方法锁定了Response,导致ServeJSON无法修改Response

func (r *Response) WriteHeader(code int) {
	// 判断状态码是否大于0
	if r.Status > 0 {
		//prevent multiple response.WriteHeader calls
		return
	}
	r.Status = code	// 设置状态码
	r.Started = true // 其他都很正常,就这个started不太清楚
	r.ResponseWriter.WriteHeader(code)
}

可见started变量不太清楚,所以我们找一下这个变量用途,我们可以在相同文件(context.go)中看到一个Response结构体中有一段对started变量的解释:

started set to true if response was written to then don’t execute other handler(如果started变量设置为true时就不会再执行其他方法)

这里可以大致猜出来,started变量用于识别Response是否已经发出(锁定?),意味着调用WriteHeader方法就会锁定Response不能再修改。再看一下ServeJSON方法

// ServeJSON sends a json response with encoding charset.
func (c *Controller) ServeJSON(encoding ...bool) {
	var (
		hasIndent   = BConfig.RunMode != PROD
		hasEncoding = len(encoding) > 0 && encoding[0]
	)
	// 这里调用了c.Ctx.Output.JSON方法,我们直接看这个方法
	c.Ctx.Output.JSON(c.Data["json"], hasIndent, hasEncoding)
}
<<==============================================>>
// JSON writes json to response body.
// if encoding is true, it converts utf-8 to \u0000 type.
func (output *BeegoOutput) JSON(data interface{}, hasIndent bool, encoding bool) error {
	// 最重要的是这一句,设置了Header
	output.Header("Content-Type", "application/json; charset=utf-8")
	......
	return output.Body(content)
}

我们可以看到ServeJSON方法中设置了Header,但是在此之前我们已经使用了WriteHeader方法导致Response锁定了,所以这里设置Header自然失效。


5.解决问题

继续看源码,发现Beego的作者已经提供了解决方法。就在上面JSON方法最终return的output.Body方法中。下面发出的是重要一部分,源码中其它代码,请自行查阅。

func (output *BeegoOutput) Body(content []byte) error {
	......
	// Write status code if it has been set manually
	// Set it to 0 afterwards to prevent "multiple response.WriteHeader calls"
	// 如果Status Code被设置就写入
	// 然后设置为0,防止多个response.WriteHeader被执行
	if output.Status != 0 {
		// 此处调用了WriteHeader方法
		output.Context.ResponseWriter.WriteHeader(output.Status)
		output.Status = 0
	} else {
		// 如果没有设置Status Code就锁定Response
		output.Context.ResponseWriter.Started = true
	}
	.......
	return nil
}

所以如果我们使用ServeJSON方法的同时需要设置HTTP Status Code,只需要设置output.Status就可以了。如下所示:

// 测试代码
func (c *YourController) Get() {
	c.Ctx.Output.Status = 405
	// c.Ctx.ResponseWriter.WriteHeader(405)
	c.Data["json"] = data
	c.ServeJSON()
}

Postman测试截图

可见Content-Type成功顺利被设置为application/json; charset=utf-8,同时HTTP Status Code显示正常。Postman测试截图


网上资料比较少,此文章水平不堪,望能帮助各位,如果有问题请在下方留言。