服务端必须手动解压gzip请求体,因Go标准库http.Server不自动处理;需先检查Content-Encoding头是否为gzip,再用gzip.NewReader包装r.Body,并defer reader.Close()。

请求体是gzip压缩时,服务端必须手动解压
Go标准库的http.Server不会像客户端那样自动解压请求体——Accept-Encoding和Content-Encoding在服务端是完全不互通的。如果你收到带Content-Encoding: gzip的POST请求,r.Body仍是原始压缩流,直接读取会得到乱码或gzip: invalid header错误(因为没解压就当普通字节流用了)。
正确做法是检查头、选择解压器、再套一层io.Reader:
- 先用
r.Header.Get("Content-Encoding")判断是否为gzip(注意大小写不敏感) - 若是,用
gzip.NewReader(r.Body)包装,得到可读的解压后流 - 若不是,直接用
r.Body;记得统一defer r.Body.Close(),避免泄漏 - 别忘了
gzip.NewReader可能返回错误(如损坏流),需提前检查
Base64编码的请求体不能用DecodeString
base64.StdEncoding.DecodeString()只接受string,而r.Body是io.ReadCloser。强行ioutil.ReadAll(r.Body)再转字符串,不仅类型绕弯,还会把整个Base64文本和解码后二进制数据同时塞进内存——上传10MB图片时,内存峰值接近20MB。
应该用流式解码器直接桥接:
立即学习“go语言免费学习笔记(深入)”;
- 调用
base64.NewDecoder(base64.StdEncoding, r.Body),返回一个io.Reader - 这个返回值可直接传给
io.Copy(dst, dec),比如写入文件或上传到对象存储 - 如果需要校验或截断,可在
io.Copy前加io.LimitReader(dec, maxBytes) - 注意:Base64编码末尾可能有换行或空格,
NewDecoder能自动跳过,但DecodeString会报illegal base64 data
Content-Type不匹配会导致解码逻辑失效
服务端不能只看请求体内容就决定怎么解压或解码——必须结合Content-Type和Content-Encoding共同判断。常见组合:
-
Content-Type: application/json+Content-Encoding: gzip→ 先gzip解压,再json.Unmarshal -
Content-Type: text/plain+ Base64内容 → 用base64.NewDecoder,不是DecodeString -
Content-Type: application/x-www-form-urlencoded→ 用r.ParseForm(),别自己读r.Body -
Content-Type: multipart/form-data→ 必须用r.ParseMultipartForm(),否则r.Body已被消费无法重读
漏判Content-Type容易导致「看似解压成功,实际JSON解析失败」这类静默错误。
多个编码/压缩层叠加时顺序不能错
极少数场景下,客户端可能先Base64再gzip(虽然不合理,但真实存在)。此时解码顺序必须严格逆向:先gzip解压,再Base64解码。反过来会失败——因为gzip输出是二进制,不是合法Base64字符集。
判断依据只有请求头:Content-Encoding: gzip一定在外层,Base64属于应用层编码,无标准头标识,只能靠业务约定或Content-Type后缀(如application/octet-stream+base64)。
实操建议:
- 用
bytes.Buffer临时接收第一层解压/解码结果,再喂给下一层,避免嵌套太深难以调试 - 对每层都加长度限制(
io.LimitReader)和超时控制(http.Request.Context().Done()) - 日志里记录原始
Content-Length、解压后长度、Base64解码后长度,三者差异能快速定位哪层出问题
最易忽略的是:无论哪层,r.Body.Close()只能调一次,且必须在所有读操作结束后——流式处理中提前关闭会导致后续Read返回io.EOF或http: read on closed response body。


















