C#中FormUrlEncodedContent转空格为%20适配Discord OAuth认证
C# HttpClient对接Discord OAuth2的Scope编码问题
问题背景
对接Discord的OAuth2 Client Credentials Grant端点时,Discord要求Scope参数的分隔符必须是%20(URL编码后的空格),但默认使用FormUrlEncodedContent提交表单时,HttpClient会将空格转换为+,导致请求返回400 Bad Request,错误信息为:
{"error": "invalid_scope", "error_description": "The requested scope is invalid, unknown, or malformed."}
如果提前用Uri.EscapeDataString对Scope字符串编码,HttpClient会再次编码,把%20转为%2520,同样无法通过验证:
POST https://discord.com/api/v10/oauth2/token HTTP/1.1 Host: discord.com Authorization: Basic VerySecret Content-Type: application/x-www-form-urlencoded Content-Length: 65 grant_type=client_credentials&scope=identify%2520guilds%2520email
原核心代码片段:
var scopeasStr = string.Join(" ", opts.Scopes); var nvc = new List<KeyValuePair<string, string>>(); nvc.Add(new KeyValuePair<string, string>("grant_type", opts.GrantType)); nvc.Add(new KeyValuePair<string, string>("scope", scopeasStr)); var content = new FormUrlEncodedContent(nvc); var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{apiUrl}/oauth2/token"); requestMessage.Content = content;
解决方案
方案1:自定义表单内容类,替换默认编码逻辑
创建自定义的CustomFormUrlEncodedContent类,使用Uri.EscapeDataString处理键值对,确保空格转为%20:
public class CustomFormUrlEncodedContent : ByteArrayContent { public CustomFormUrlEncodedContent(IEnumerable<KeyValuePair<string, string>> nameValueCollection) : base(GetContentByteArray(nameValueCollection)) { Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); } private static byte[] GetContentByteArray(IEnumerable<KeyValuePair<string, string>> nameValueCollection) { if (nameValueCollection == null) throw new ArgumentNullException(nameof(nameValueCollection)); var encodedPairs = nameValueCollection.Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}"); var contentString = string.Join("&", encodedPairs); return Encoding.UTF8.GetBytes(contentString); } }
替换原代码中的FormUrlEncodedContent:
// 替换原FormUrlEncodedContent实例化代码 var content = new CustomFormUrlEncodedContent(nvc);
方案2:手动构建表单内容字符串
直接拼接符合要求的表单内容,用StringContent发送:
// 直接用%20拼接Scope,无需额外编码 var scopeasStr = string.Join("%20", opts.Scopes); // 手动拼接完整表单内容 var formContent = $"grant_type={Uri.EscapeDataString(opts.GrantType)}&scope={scopeasStr}"; var content = new StringContent(formContent, Encoding.UTF8, "application/x-www-form-urlencoded");
方案3:避免HttpClient二次编码的拼接方式
先将Scope用%20拼接,再手动构建表单字符串:
var scopeasStr = string.Join("%20", opts.Scopes); var nvc = new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("grant_type", opts.GrantType), new KeyValuePair<string, string>("scope", scopeasStr) }; // 手动编码每个键值对并拼接 var formString = string.Join("&", nvc.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); var content = new StringContent(formString, Encoding.UTF8, "application/x-www-form-urlencoded");
原理说明
FormUrlEncodedContent遵循HTTP标准,将空格转为+,但Discord OAuth端点仅支持%20作为Scope分隔符。上述方案通过自定义编码逻辑或手动构建内容,绕过了HttpClient的默认编码,确保生成的Scope参数符合Discord的要求。
内容的提问来源于stack exchange,提问作者Reinard
相关产品推荐
相关产品推荐

