Go函数传*strings.Reader类型payload请求token报grant_type缺失如何解决
异常产生原因
*strings.Reader 是维护了内部读取偏移量的流类型,并非静态值容器。当你把同一个*strings.Reader实例传入函数构造HTTP请求时,http.NewRequest会完整读取Reader内的全部内容用来生成请求体,读取完成后该Reader的内部偏移指针会直接停在内容末尾。
后续重复传入同一个Reader实例时,请求构造流程从当前偏移位置(也就是内容末尾)开始读取,最终拿到的请求体是空值,服务端解析不到任何表单参数,就会返回Missing form parameter: grant_type的错误。
你在函数内部直接定义strings.Reader时每次调用都会生成偏移量在起始位置的全新实例,所以能正常运行;仅通过反射校验类型无法感知Reader的内部偏移状态,因此之前没定位到根因。注意这个问题和参数是否为指针类型无关,哪怕传入值类型的strings.Reader,只要实例的读取偏移不在起始位置,一样会出现读空内容的问题。
正确传参方案
根据代码复用的需求,选以下任意一种方案修改即可:
- 方案1(推荐):传递原始表单字符串或结构化参数,在函数内部生成全新Reader
把函数入参从*strings.Reader改为字符串、map等静态值类型,每次调用时在函数内构造新的Reader实例,从根源上避免偏移量问题,逻辑最清晰也最不容易出错。 - 方案2:传递Reader前重置偏移量
如果必须传递*strings.Reader类型,每次调用函数前先执行payload.Seek(0, io.SeekStart)把Reader的读取指针移到内容起始位置,再传入函数即可,需要提前引入io标准库。 - 方案3(最佳实践):用标准库构造表单参数
不要手动拼接表单字符串,直接用net/url标准库的Values类型构造表单参数,自动处理特殊字符转义,同时每次请求都生成全新的请求体Reader,完全规避偏移量问题。
修正后的参考代码
import ( "io/ioutil" "log" "net/http" "net/url" "strings" "github.com/tidwall/gjson" ) // 直接接收结构化表单参数,内部构造请求,复用性更强 func getTokenAgain(formParams map[string]string) string { tokenUrl := "https://my-api-url.com/auth/realms/CP/protocol/openid-connect/token" // 用标准库构造表单数据,自动处理转义 formData := url.Values{} for k, v := range formParams { formData.Set(k, v) } // 每次调用都生成全新的Reader,偏移量默认在起始位置 payload := strings.NewReader(formData.Encode()) client := &http.Client{} req, err := http.NewRequest(http.MethodPost, tokenUrl, payload) if err != nil { log.Fatal(err) } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") res, err := client.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { log.Fatal(err) } accessToken := gjson.Get(string(body), "access_token") return accessToken.String() } // 调用示例 func main() { params := map[string]string{ "grant_type": "password", "username": "admin", "password": "secret", "client_id": "admin-cli", } token := getTokenAgain(params) log.Println(token) }
内容的提问来源于stack exchange,提问作者Vladimir Fomin
相关产品推荐
相关产品推荐

