You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将Xero API的Google Apps Script代码移植到C#时持续触发400错误求助

将Xero API的Google Apps Script代码移植到C#时持续触发400错误求助

我太懂这种明明GAS跑的好好的,转到C#就碰400错误的憋屈了!帮你捋捋问题根源和解决思路:

首先,Xero的Client Credentials模式获取token的接口,只认application/x-www-form-urlencoded格式的请求体,这也是你GAS代码能跑通的核心原因——GAS的UrlFetchApp会自动把你传入的payload对象编码成这种表单格式。但你最初的C#代码完全没贴合这个要求,踩了好几个坑:

你最初代码的核心问题

  1. 把请求的method、headers这些元数据,和实际要传的grant_type、scope一起序列成JSON当请求体发了——这完全是搞错了HTTP请求的结构,Xero根本看不懂
  2. 就算后来改了Content-Type,请求体的格式还是错的,接口自然返回400
  3. 读取响应内容时用了response.Content.ToString(),这玩意儿只会返回System.Net.Http.StringContent这种类型名,根本拿不到实际的响应错误信息

对比你的GAS代码(正确姿势)

你的GAS代码之所以能跑,是因为UrlFetchApp帮你做了格式转换:当你传payload为对象时,它自动把参数编码成application/x-www-form-urlencoded的表单格式,正好匹配Xero的要求。

function xeroConnect() { 
    let step = 0; 
    try { 
        const constants = globalConstants(); 
        step = 10; 
        const params = { 
            method: "post", 
            headers: {Authorization: "Basic " + Utilities.base64Encode(constants.xeroClientId + ":" + constants.xeroSecretKey)}, 
            payload: { "grant_type": "client_credentials", "scope": "accounting.transactions accounting.contacts", } 
        }; 
        step = 20; 
        let response = UrlFetchApp.fetch(constants.xeroConnectAPI, params); 
        step = 25; 
        if (!response) { 
            throw "No response"; 
        } 
        step = 30; 
        let responseValues = JSON.parse(response.getContentText()); 
        step = 40; 
        if (responseValues.HasErrors || responseValues.HasValidationErrors) { 
            throw(response.getContentText()); 
        } 
        step = 50; 
        return responseValues.access_token; 
    } catch (error) { 
        let errorString = "ERROR: xeroConnect; error=" + JSON.stringify(error) + ";step=" + step; 
        Logger.log(errorString); 
        return errorString; 
    } 
}

最终跑通的C#代码(附优化)

你最后那段代码已经能跑了,这里给个更简洁的版本,用FormUrlEncodedContent直接构造url编码的表单数据,比MultipartFormDataContent更贴合接口要求:

private static async Task<string> XeroConnect()
{
    var authBytes = Encoding.UTF8.GetBytes($"{xeroClientId}:{xeroSecretKey}");
    using var client = new HttpClient();
    
    // 设置请求超时和接受的响应格式
    client.Timeout = TimeSpan.FromSeconds(300);
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    // 设置Basic Auth头
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Basic", 
        Convert.ToBase64String(authBytes)
    );
    
    // 构造Xero要求的x-www-form-urlencoded格式请求体
    var formData = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("grant_type", "client_credentials"),
        new KeyValuePair<string, string>("scope", "accounting.transactions accounting.contacts")
    });
    
    var response = await client.PostAsync(xeroConnectAPI, formData);
    // 这里如果还是400,可以先注释掉EnsureSuccessStatusCode,打印responseContent看具体错误
    // response.EnsureSuccessStatusCode();
    var responseContent = await response.Content.ReadAsStringAsync();
    
    dynamic responseValues = JsonConvert.DeserializeObject(responseContent);
    // 可以加个错误判断,用实际的响应字段(Xero的错误响应会有error字段)
    if (responseValues?.error != null)
    {
        throw new Exception($"Xero认证失败: {responseValues.error_description ?? responseValues.error}");
    }
    
    return responseValues.access_token;
}

额外排查技巧

如果之后再碰400错误,先注释掉response.EnsureSuccessStatusCode(),然后打印responseContent的内容——Xero的错误响应里一般会有error和error_description字段,能告诉你具体是哪里错了,比如scope无效、认证信息不对之类的,比瞎猜靠谱多了!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 11:35:30