这是一个奇怪但是很常见的问题,当你试图使用 Go 去访问一些服务时,你会发现它会经常出错,哪怕在 curl 下经过了千锤百炼的接口,或是已经上线正常用了多年的系统,当你用 Go 去请求时,依然会时不时的超出你的预期。
当前 License 云端服务就遇到了这种情况,该服务采用 Ktor
开发,并且包含了以下接口:
/api/license/cloud/component/one2
该接口接受两个参数,并返回指定组件的信息,其 curl 请求形式如下:
$ curl 'http://0.0.0.0:9990/api/license/cloud/component/one2?compName=sample&compKey=abcdefg' -v
可以正常返回一个 json 字符串,照理说到这里,是完全没有问题的,但是在 Go 的请求下,却出现了问题:
func httpGet(url string) string {
resp, err := http.Get(url)
if err != nil {
fmt.Printf("1 => %v, resp = %v \n", err, resp)
return ""
}
if resp.StatusCode != 200 {
fmt.Printf("2 => %d \n", resp.StatusCode)
return ""
}
buf, err := ioutil.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
fmt.Printf("3 => %v \n", err)
return ""
}
return string(buf)
}
很不幸的,打印出了以下异常:
net/http: HTTP/1.x transport connection broken: too many transfer encodings: ["chunked" "chunked"]
看到这个,我第一个反应是,看一眼 Response 的 Header 里面是否有重复的 Transfer-Encoding: chunked
,具体看的情况如下:
$ curl --compressed ... ... 将产生重复的 Transfer-Encoding: chunked
$ curl ... ... 不产生 Transfer-Encoding: chunked
看起来问题似乎出在响应压缩上?是的,可以先明确的问题是这个,Go 的 HTTP 标准库内的请求方式也是带了压缩,也就是说解决方案是以下两种:
Ktor 响应时不写入 Transfer-Encoding: chunked
Go 请求时要求不压缩
那么就按这两种方法,逐一讨论吧。
从 Ktor 入手
在 Ktor
文档里,有如下的明确说明:
很明确的说了,如果返回的内容中,Content-Length
为 null,则会写入 Transfer-Encoding: chunked
。
那我直接把代码改成这样,是不是就避免了问题呢:
/* 根据组件名称和key获取一个组件的详情 */
get("/one2") {
val p = call.requestParameters()
val name = p["compName"] ?: ""
val key = p["compKey"] ?: ""
val ret = application.compRegister.get(name, key)
val result = Result(200, "", ret)
val retstr = objectMap.writeValueAsString(result)
call.respond(object: OutgoingContent.WriteChannelContent() {
override val contentLength = retstr.utf8Size()
override val contentType = ContentType.Application.Json
override suspend fun writeTo(channel: ByteWriteChannel) {
channel.writeStringUtf8(retstr)
}
})
}
但是很无奈的是,这么做并无助于解决问题,响应头内依然有两条重复的 Transfer-Encoding: chunked
。
那就只能从另一方面去解决了,在 Ktor
的 Application
内,将 Compression
插件整个去除。
经过去除 Compression
插件,返回的数据即是正确的。读了一下该插件的源码,确实是它带来了这样的问题,还需等待后续 Ktor 版本更新来解决了。
顺便再说一句,当去除 Compression
插件后,即使在 /one2
接口中,不写 contentLength
,也不会返回 Transfer-Encoding: chunked
,所以,这文档是写了个寂寞?
从 Go 入手
从 Go 入手,办法就多了,只要阻止请求时压缩即可,以下列举两种。
方法一:
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Accept-Encoding","identity")
client:= & http.Client {}
resp, _ := client.Do(req)
方法二:
tr := &http.Transport {
DisableCompression:true,
}
client := &http.Client {Transport:tr}
req, _ := http.NewRequest("GET", url, nil)
resp, _ := client.Do(req)
至此,Transfer-Encoding: chunked
的问题被解决。当然了,如果你依然觉得麻烦并且不想修改任何一行代码,你也可以直接将 Go 的版本更换为 1.13 以下,在老版本的 Go SDK 中,并不会对重复的 Header 进行校验。
另外,服务端因压缩而产生的 Header 重复的问题,并非仅出现在 Ktor 下,经查在 Springboot,VertX 等常用框架下,甚至是 PHP, ASP 等技术开发的接口中也是时有存在的,在提供接口给 Go 使用时需要注意。