目录

Go 解析 HTTP Body 报错 invalid character 的原因与解决方案

问题背景

前段时间在对接一个第三方支付回调接口,本地联调阶段一切顺利,结果上线当晚就收到了告警——日志里刷出一片 invalid character 'ï' looking for beginning of value 的错误。

当时排查了大半天,从 Body 格式到字符编码逐项验证,最后才锁定了问题根源。踩完坑之后把经验整理出来,希望能帮同样遇到这个报错的朋友少走弯路。

这个错误的本质其实很简单:Go 标准库 encoding/json 在解析 HTTP Body 时,发现数据流的起始位置不是合法的 JSON 字符(比如 {[" 等),于是直接抛出异常。

典型的报错信息长这样:

invalid character 'ï' looking for beginning of value

接下来按照我实际排查的顺序,把三种最常见的触发原因逐一拆解。

原因一:JSON 格式不规范

这是最高频的情况。请求方传过来的 JSON 本身就不合法,常见的写法问题包括:

  • 字段名或字符串值用了单引号,而 JSON 标准只允许双引号
  • 末尾多了一个逗号(trailing comma)
  • 值里混入了未转义的特殊字符

遇到这种情况,不建议拿到 Body 就直接丢给 json.Unmarshal。更稳妥的做法是先把原始内容打印或记录下来,肉眼确认格式是否正确,再进行解析。

bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
    log.Printf("读取 Body 失败: %v", err)
    return
}

// 先记录原始内容,方便排查
log.Printf("原始 Body: %s", string(bodyBytes))

// 快速校验是否为合法 JSON
if !json.Valid(bodyBytes) {
    log.Printf("Body 不是合法的 JSON 格式")
    return
}

var result map[string]interface{}
if err := json.Unmarshal(bodyBytes, &result); err != nil {
    log.Printf("JSON 解析失败: %v", err)
    return
}

这里有个小技巧:json.Valid() 可以在不做完整反序列化的前提下快速判断数据是否为合法 JSON,排查效率比直接 Unmarshal 再看报错信息高不少。

如果确认是对方传过来的格式有问题,就需要联系接口提供方修正,这个没有什么取巧的办法。

原因二:字符编码不匹配

如果 JSON 格式本身没有问题,那就要怀疑编码了。

Go 的 encoding/json 包默认按 UTF-8 编码处理输入数据。当请求方使用 GBK 或 GB2312 等编码传递中文内容时,Go 会把那些非 UTF-8 字节当作非法字符,从而触发 invalid character 错误。

解决思路很直接——在解析之前先做一次编码转换,把 GBK 字节流转成 UTF-8。需要用到 golang.org/x/text 这个官方扩展库:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"

    "golang.org/x/text/encoding/simplifiedchinese"
    "golang.org/x/text/transform"
)

type CallbackData struct {
    OrderID string `json:"order_id"`
    Status  string `json:"status"`
}

// gbkToUTF8 将 GBK 编码的字节切片转换为 UTF-8
func gbkToUTF8(src []byte) ([]byte, error) {
    reader := transform.NewReader(
        bytes.NewReader(src),
        simplifiedchinese.GBK.NewDecoder(),
    )
    return io.ReadAll(reader)
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
    bodyBytes, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "读取 Body 失败", http.StatusBadRequest)
        return
    }

    var data CallbackData

    // 先按 UTF-8 尝试解析
    if err := json.Unmarshal(bodyBytes, &data); err != nil {
        // UTF-8 解析失败,尝试按 GBK 转码后再解析
        utf8Bytes, convErr := gbkToUTF8(bodyBytes)
        if convErr != nil {
            http.Error(w, fmt.Sprintf("编码转换失败: %v", convErr), http.StatusBadRequest)
            return
        }
        if err := json.Unmarshal(utf8Bytes, &data); err != nil {
            http.Error(w, fmt.Sprintf("JSON 解析失败: %v", err), http.StatusBadRequest)
            return
        }
    }

    fmt.Fprintf(w, "处理成功,订单号: %s", data.OrderID)
}

func main() {
    http.HandleFunc("/callback", callbackHandler)
    _ = http.ListenAndServe(":8080", nil)
}

实际项目中可以根据请求头 Content-Type 里的 charset 参数来判断对方使用的编码,而不是每次都盲猜。比如对方传了 Content-Type: application/json; charset=gbk,就可以直接走 GBK 转换逻辑。

原因三:Body 中存在 BOM 头或不可见字符

这正是我那次线上事故的真正元凶。

有些系统(尤其是 Windows 环境下的服务)在生成 UTF-8 内容时会自动在开头插入一段 BOM(Byte Order Mark),对应的字节序列是 \xEF\xBB\xBF。这三个字节在文本编辑器里看不到,但 Go 的 JSON 解析器会把它们当成非法字符,直接报错。

报错信息里的 invalid character 'ï' 就是 0xEF 这个字节按 Latin-1 编码显示出来的样子——看到 ï 基本可以确定是 BOM 头在作怪。

处理方式也不复杂,解析之前检测并移除 BOM 头即可:

// removeBOM 移除 UTF-8 BOM 头
func removeBOM(data []byte) []byte {
    if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
        return data[3:]
    }
    return data
}

如果你不确定 Body 里还有没有其他不可见字符干扰,可以用一个更通用的清理函数:

import "regexp"

// cleanBody 移除 BOM 头和其他不可见控制字符
func cleanBody(data []byte) []byte {
    // 移除 UTF-8 BOM
    if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
        data = data[3:]
    }
    // 过滤不可见控制字符,保留空格、换行、制表符
    re := regexp.MustCompile(`[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]`)
    return re.ReplaceAllLiteral(data, []byte(""))
}

在实际使用时,把这个函数放在 json.Unmarshal 之前调用就行:

bodyBytes, _ := io.ReadAll(r.Body)
bodyBytes = cleanBody(bodyBytes)

var result map[string]interface{}
json.Unmarshal(bodyBytes, &result)

原因四:Body 被重复读取

这个原因容易被忽略。Go 的 http.Request.Body 是一个 io.ReadCloser,只能读取一次。如果在中间件或拦截器中已经读取过 Body,后续的处理函数再读取时拿到的就是空数据,传给 json.Unmarshal 同样会报 invalid character 或者 unexpected end of JSON input

解决办法是读取后把内容写回去:

bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
    return
}
// 把内容写回 Body,让后续代码还能读取
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))

如果项目里用了 Gin、Echo 等框架,部分框架已经内置了 Body 缓存机制,具体可以查阅对应框架的文档。

完整排查流程

把上面四种情况串起来,可以按照这个顺序逐步排查:

  1. 打印原始 Body — 用 log.Printf 或者日志库记录原始字节内容,确认拿到的数据是什么
  2. 检查 JSON 格式 — 用 json.Valid() 快速校验,或者把内容复制到在线 JSON 校验工具里验证
  3. 检查 BOM 头 — 查看前三个字节是否为 0xEF 0xBB 0xBF,如果是就移除
  4. 确认字符编码 — 检查 Content-Type 头里的 charset,必要时做编码转换
  5. 排除重复读取 — 确认 Body 没有在中间件或其他位置被提前消费

常见问题

为什么本地测试正常,上线后才报错?

本地测试时通常是自己构造的请求数据,编码和格式都是标准的 UTF-8 JSON。而线上环境接收的是第三方系统发过来的数据,对方的服务器可能运行在 Windows 上,输出时自动带了 BOM 头,或者使用了 GBK 编码。这种差异只有在真实对接时才会暴露出来。

invalid character 'ï'invalid character '<' 是一回事吗?

不是。ï 通常指向 BOM 头问题(0xEF 的 Latin-1 显示),而 < 一般意味着对方返回的不是 JSON,而是 HTML 页面(比如 404 页面或者错误页面)。遇到 < 开头的情况,先检查请求的 URL 和参数是否正确。

json.NewDecoder 替代 json.Unmarshal 能避免这个问题吗?

不能。json.NewDecoderjson.Unmarshal 底层使用的是同一套解析逻辑,遇到非法字符同样会报错。不过 json.NewDecoder 适合处理流式数据或者 Body 内容较大的场景,性能上会更好一些。

如何判断 Body 中是否包含 BOM 头?

读取 Body 后,检查前三个字节是否等于 0xEF0xBB0xBF。也可以用 hex.Dump 把前几个字节打印出来直观查看:

bodyBytes, _ := io.ReadAll(r.Body)
fmt.Printf("前 10 个字节的十六进制: %x\n", bodyBytes[:10])

如果输出以 efbbbf 开头,就说明存在 BOM 头。

总结

invalid character 这个报错看着吓人,实际上排查起来并不复杂。核心思路就一条:先看原始数据,再做解析

按优先级排列,四种常见原因分别是:

  1. JSON 格式本身不合法 — 最常见,联系对方修正即可
  2. BOM 头或不可见字符 — 解析前做一次清理
  3. 字符编码不匹配 — 检测 charset 并做编码转换
  4. Body 被重复读取 — 读取后写回,或使用框架的缓存机制

养成一个习惯:对接外部接口时,永远先把原始 Body 记录到日志里。这样不管遇到什么奇怪的解析问题,都能第一时间拿到现场数据,排查效率会高很多。

如果大家在 Go 开发中遇到其他 HTTP Body 解析相关的坑,或者有更好的处理方式,欢迎在评论区交流讨论~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-http-body-invalid-character-error-fix/

备用原文链接: https://blog.fiveyoboy.com/articles/go-http-body-invalid-character-error-fix/