Go调用Asana OAuth接口返回400错误,Node.js代码正常运行
我有一段可正常运行的Node.js代码,用于生成Asana OAuth授权链接以及获取访问令牌:
export const createAsanaAuthUrl = () => { // Store this in DB const codeChallenge = crypto .createHash('sha256') .update(CODE_VERIFIER) .digest('base64') .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); const udid = uuidv4(); const url = `${ASANA_AUTH_BASE}/oauth_authorize?client_id=${ASANA_CLIENT_ID}&redirect_uri=${ASANA_REDIRECT_URL}&response_type=code&state=${udid}&code_challenge_method=S256&code_challenge=${codeChallenge}`; return url; }; export const getAsanToken = async (code: string) => { const params = { grant_type: 'authorization_code', client_id: ASANA_CLIENT_ID, client_secret: ASANA_CLIENT_SECRET, redirect_uri: ASANA_REDIRECT_URL, code, code_verifier: CODE_VERIFIER, }; const { refresh_token, access_token } = ( await axios.post(`${ASANA_AUTH_BASE}/oauth_token`, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }) ).data; };
但对应的Go代码调用Asana的/oauth_token接口时返回400错误,代码如下:
func CreateAsanaAuthUrl(env *config.Env) string { hash := sha256.Sum256([]byte(constants.AsanaCodeVerifier)) codeChallenge := base64.URLEncoding.EncodeToString(hash[:]) codeChallenge = strings.TrimRight(codeChallenge, "=") udid := uuid.New().String() url := fmt.Sprintf("%s/oauth_authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s&code_challenge_method=S256&code_challenge=%s", constants.AsanaAuthBase, env.AsanaClientId, env.AsanaRedirectUrl, udid, codeChallenge) return url } func GetAsanaToken (args domain.GetTokenArgs) (*domain.ServiceAuthReturn, error) { env := args.Env params := TokenAsanaRequest{ GrantType: "authorization_code", ClientID: env.AsanaClientId, ClientSecret: env.AsanaClientSecret, RedirectURI: env.AsanaRedirectUrl, Code: args.Code, CodeVerifier: constants.AsanaCodeVerifier, } fmt.Printf("p: %+v\n", params) url := constants.AsanaAuthBase + "/oauth_token" reqBody, err := json.Marshal(params) fmt.Printf("Url: %v", url) req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, err := client.Do(req) if err != nil { log.Println("Error", err) // log return nil, fmt.Errorf("error sending Asana auth request: %s", err.Error()) } defer resp.Body.Close() if resp.StatusCode >= 400 { // server returned an error errorBody, err := ioutil.ReadAll(resp.Body) if err != nil { log.Println("Error:", err) // log return nil, fmt.Errorf("error reading error response body: %s", err.Error()) } log.Printf("Error response body: %s", string(errorBody)) // log error response body return nil, fmt.Errorf("error getting asana auth token %d", resp.StatusCode) } var result struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } }
错误发生在日志打印错误响应体的位置,状态码为400,请问可能是什么原因导致的?
原因分析
请求体格式不匹配:Go代码中用
json.Marshal生成JSON格式的请求体,但设置的Content-Type是application/x-www-form-urlencoded,二者完全矛盾。Asana的OAuth令牌接口要求接收表单格式的请求体(如grant_type=authorization_code&client_id=xxx),但你实际发送的是JSON,服务器无法解析,直接返回400错误。Node.js与Go的行为差异:Node.js中的axios库,当传入普通对象并设置
Content-Type: application/x-www-form-urlencoded时,会自动将对象序列化为表单格式;但Go没有这种自动转换逻辑,手动用JSON序列化后发送,就会出现格式不兼容的问题。字段名可能不匹配:如果
TokenAsanaRequest结构体没有添加表单序列化标签(如form:"grant_type"),即使用表单序列化,字段名可能会变成驼峰式(如GrantType),而Asana接口需要的是下划线格式的字段名,也会导致解析失败。
修复方案
修改Go代码中的请求体生成逻辑,将参数序列化为表单格式:
// 替换原有的json.Marshal相关代码 form := url.Values{} form.Set("grant_type", "authorization_code") form.Set("client_id", env.AsanaClientId) form.Set("client_secret", env.AsanaClientSecret) form.Set("redirect_uri", env.AsanaRedirectUrl) form.Set("code", args.Code) form.Set("code_verifier", constants.AsanaCodeVerifier) reqBody := strings.NewReader(form.Encode()) // 创建请求时使用表单体 req, err := http.NewRequest("POST", url, reqBody) if err != nil { log.Println("Error creating request:", err) return nil, fmt.Errorf("error creating Asana auth request: %s", err.Error()) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
如果想用结构体直接转表单,可以使用第三方库(如github.com/go-playground/form),但手动构建url.Values是最直接的修复方式。
内容的提问来源于stack exchange,提问作者Alwaysblue

