将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#代码完全没贴合这个要求,踩了好几个坑:
你最初代码的核心问题
- 把请求的
method、headers这些元数据,和实际要传的grant_type、scope一起序列成JSON当请求体发了——这完全是搞错了HTTP请求的结构,Xero根本看不懂 - 就算后来改了Content-Type,请求体的格式还是错的,接口自然返回400
- 读取响应内容时用了
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
相关产品推荐
相关产品推荐

