通过模拟 HttpResponseMessage 的 C# 字符串编码问题
C# string encoding issue via mocked HttpResponseMessage
我正在尝试测试从远程 API 获取数据。我按如下方式设置 HttpClient:
HttpClient httpClient = SetupHttpClient((HttpRequestMessage request) =>
{
FileStream file = new FileStream("API_Data.json"), FileMode.Open, FileAccess.Read);
StreamReader sr = new StreamReader(file, true);
var response = request.CreateResponse(HttpStatusCode.OK, sr.ReadToEnd());
response.Content.Headers.ContentEncoding.Add("UTF-8");
return Task.FromResult(response);
});
SetupHttpClient
在这里不相关 - 重要的是传递的响应,如您所见,它是通过从 FileStream 创建 StreamReader 并将该流读入响应而创建的。
使用文本可视化工具,我可以看到文件已成功读入响应流,并且所有特殊字符(例如换行符、制表符和双引号)都正确显示,如屏幕截图所示:
在另一端,我从 HttpResponseMessage 中获取内容如下:
Stream responseStream = await response.Content.ReadAsStreamAsync();
StreamReader responseReader = null;
if (response.Content.Headers.ContentEncoding.Count > 0)
responseReader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding(response.Content.Headers.ContentEncoding.First()));
else
responseReader = new StreamReader(responseStream, true);
string content = await responseReader.ReadToEndAsync();
return content;
此时再次悬停调试响应显示数据还是OK的:
Text Visualizer 显示与上面的第一个屏幕截图完全相同。问题来了 - 即使响应内容是字符串,我也无法访问值 属性 并且 response.Content 提供的所有检索机制都是通过 Streams。好的,所以我通过 Stream 获取内容,但是在通过 Stream 之后,所有特殊字符现在都经过双重转义,如您在此处所见:
这意味着我现在必须取消转义所有这些特殊字符,以便能够将返回的字符串用作 json - 如果我不取消转义,那么 JsonDeserializer 会在我尝试反序列化它。 StreamReader 还添加了一个(单转义的)双引号作为第一个和最后一个字符。
通过谷歌搜索,我所能找到的都是关于使用正确编码的参考资料。因此,我确保我将源文件保存为 UTF-8,我发送 'UTF-8' 作为 HttpResponseMessage (response.Content.Headers.ContentEncoding.Add("UTF-8");
) 的编码,并且在解码响应时 'UTF-8' 是再次用作编码 (responseReader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding(response.Content.Headers.ContentEncoding.First()));
) - 如您所见,这没有达到获得未双重转义的字符串的预期效果。
我不想在从 Stream 获取响应字符串时对所有特殊字符进行 'manual' 取消转义 - 这是一个糟糕的 hack,但感觉这是唯一的选择目前 - 如果我检测到 response.Content
是一个字符串,或者使用反射来获取 response.Content.Value
属性 的内容 - 这又是另一个我不想做的 hack。
如何确保在通过 StreamReader 获取 response.Content
值时不会得到双重转义的特殊字符?
编辑:为清楚起见,这里是 SetupHttpClient 方法:
public HttpClient SetupHttpClient(Func<HttpRequestMessage, Task<HttpResponseMessage>> response)
{
var configuration = new HttpConfiguration();
var clientHandlerStub = new HttpDelegatingHandlerStub((request, cancellationToken) =>
{
request.SetConfiguration(configuration);
return response(request);
});
HttpClient httpClient = new HttpClient(clientHandlerStub);
mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);
return httpClient;
}
和 HttpDelegatingHandlerStub
public class HttpDelegatingHandlerStub : DelegatingHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
public HttpDelegatingHandlerStub()
{
_handlerFunc = (request, cancellationToken) => Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
}
public HttpDelegatingHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc)
{
_handlerFunc = handlerFunc;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _handlerFunc(request, cancellationToken);
}
}
EDIT2:一个最小的、可重现的例子——这需要以下包——Microsoft.AspNet.WebApi.Core、Microsoft.Extensions.Http、最小起订量:
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using Moq;
namespace StreamReaderEncoding
{
internal class Program
{
static Mock<IHttpClientFactory> mockHttpClientFactory;
static void Main(string[] args)
{
MainAsync().Wait();
}
static async Task MainAsync()
{
mockHttpClientFactory = new Mock<IHttpClientFactory>();
string content = @"{
""test"": ""true""
}";
Console.WriteLine($"content before: {content}");
HttpClient httpClient = SetupHttpClient((HttpRequestMessage request) =>
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(content);
writer.Flush();
stream.Position = 0;
StreamReader sr = new StreamReader(stream, true);
var response = request.CreateResponse(HttpStatusCode.OK, sr.ReadToEnd());
response.Content.Headers.ContentEncoding.Add("UTF-8");
return Task.FromResult(response);
});
HttpResponseMessage response = await httpClient.GetAsync("https://www.test.com");
Stream responseStream = await response.Content.ReadAsStreamAsync();
StreamReader responseReader = null;
if (response.Content.Headers.ContentEncoding.Count > 0)
responseReader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding(response.Content.Headers.ContentEncoding.First()));
else
responseReader = new StreamReader(responseStream, true);
content = await responseReader.ReadToEndAsync();
Console.WriteLine($"content after: {content}");
}
static HttpClient SetupHttpClient(Func<HttpRequestMessage, Task<HttpResponseMessage>> response)
{
var configuration = new HttpConfiguration();
var clientHandlerStub = new HttpDelegatingHandlerStub((request, cancellationToken) =>
{
request.SetConfiguration(configuration);
return response(request);
});
HttpClient httpClient = new HttpClient(clientHandlerStub);
mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);
return httpClient;
}
}
internal class HttpDelegatingHandlerStub : DelegatingHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
public HttpDelegatingHandlerStub()
{
_handlerFunc = (request, cancellationToken) => Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
}
public HttpDelegatingHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc)
{
_handlerFunc = handlerFunc;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _handlerFunc(request, cancellationToken);
}
}
}
示例的输出:
content before: {
"test": "true"
}
content after: "{\r\n \"test\": \"true\"\r\n}"
这与起订量无关...完全是关于 HttpRequestMessageExtensions.CreateResponse()
,它将您的字符串编码为 JSON。
这是一个更简单的示例(作为 .NET 6 控制台应用程序;您可以忍受它抱怨恢复 net461 的目标,或者将其重新定位为 .NET 4.7.1 或类似版本并添加一些项目选项和 using 指令;我认为让它以 .NET 6 为目标更简单。)
using System.Web.Http;
var request = new HttpRequestMessage();
request.SetConfiguration(new HttpConfiguration());
string json = "{ \"test\": \"true\" }";
Console.WriteLine($"Before: {json}");
var response = request.CreateResponse(json);
string text = await response.Content.ReadAsStringAsync();
Console.WriteLine($"After: {text}");
输出:
Before: { "test": "true" }
After: "{ \"test\": \"true\" }"
我认为让您感到困惑的是,在调试器中,您正在查看 response.Content
,将 ObjectContent<string>
中的 Value
视为字符串 如您所愿,并假设这是要写入响应的数据。它不是。这是在格式化之前的数据。
解决此问题的最简单方法是将响应内容提供为 StringContent
。至此你不需要 any 依赖 - 下面的代码是一个最小的例子,它为“之前”和“之后”打印相同的文本:
var request = new HttpRequestMessage();
string json = "{ \"test\": \"true\" }";
Console.WriteLine($"Before: {json}");
var response = new HttpResponseMessage { Content = new StringContent(json) };
string text = await response.Content.ReadAsStringAsync();
Console.WriteLine($"After: {text}");
当然你可能想在响应中设置一些其他的 headers,但我相信这证明它确实是 CreateResponse
方法(及其对 ObjectContent
的使用) 导致了问题。
我正在尝试测试从远程 API 获取数据。我按如下方式设置 HttpClient:
HttpClient httpClient = SetupHttpClient((HttpRequestMessage request) =>
{
FileStream file = new FileStream("API_Data.json"), FileMode.Open, FileAccess.Read);
StreamReader sr = new StreamReader(file, true);
var response = request.CreateResponse(HttpStatusCode.OK, sr.ReadToEnd());
response.Content.Headers.ContentEncoding.Add("UTF-8");
return Task.FromResult(response);
});
SetupHttpClient
在这里不相关 - 重要的是传递的响应,如您所见,它是通过从 FileStream 创建 StreamReader 并将该流读入响应而创建的。
使用文本可视化工具,我可以看到文件已成功读入响应流,并且所有特殊字符(例如换行符、制表符和双引号)都正确显示,如屏幕截图所示:
在另一端,我从 HttpResponseMessage 中获取内容如下:
Stream responseStream = await response.Content.ReadAsStreamAsync();
StreamReader responseReader = null;
if (response.Content.Headers.ContentEncoding.Count > 0)
responseReader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding(response.Content.Headers.ContentEncoding.First()));
else
responseReader = new StreamReader(responseStream, true);
string content = await responseReader.ReadToEndAsync();
return content;
此时再次悬停调试响应显示数据还是OK的:
Text Visualizer 显示与上面的第一个屏幕截图完全相同。问题来了 - 即使响应内容是字符串,我也无法访问值 属性 并且 response.Content 提供的所有检索机制都是通过 Streams。好的,所以我通过 Stream 获取内容,但是在通过 Stream 之后,所有特殊字符现在都经过双重转义,如您在此处所见:
这意味着我现在必须取消转义所有这些特殊字符,以便能够将返回的字符串用作 json - 如果我不取消转义,那么 JsonDeserializer 会在我尝试反序列化它。 StreamReader 还添加了一个(单转义的)双引号作为第一个和最后一个字符。
通过谷歌搜索,我所能找到的都是关于使用正确编码的参考资料。因此,我确保我将源文件保存为 UTF-8,我发送 'UTF-8' 作为 HttpResponseMessage (response.Content.Headers.ContentEncoding.Add("UTF-8");
) 的编码,并且在解码响应时 'UTF-8' 是再次用作编码 (responseReader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding(response.Content.Headers.ContentEncoding.First()));
) - 如您所见,这没有达到获得未双重转义的字符串的预期效果。
我不想在从 Stream 获取响应字符串时对所有特殊字符进行 'manual' 取消转义 - 这是一个糟糕的 hack,但感觉这是唯一的选择目前 - 如果我检测到 response.Content
是一个字符串,或者使用反射来获取 response.Content.Value
属性 的内容 - 这又是另一个我不想做的 hack。
如何确保在通过 StreamReader 获取 response.Content
值时不会得到双重转义的特殊字符?
编辑:为清楚起见,这里是 SetupHttpClient 方法:
public HttpClient SetupHttpClient(Func<HttpRequestMessage, Task<HttpResponseMessage>> response)
{
var configuration = new HttpConfiguration();
var clientHandlerStub = new HttpDelegatingHandlerStub((request, cancellationToken) =>
{
request.SetConfiguration(configuration);
return response(request);
});
HttpClient httpClient = new HttpClient(clientHandlerStub);
mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);
return httpClient;
}
和 HttpDelegatingHandlerStub
public class HttpDelegatingHandlerStub : DelegatingHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
public HttpDelegatingHandlerStub()
{
_handlerFunc = (request, cancellationToken) => Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
}
public HttpDelegatingHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc)
{
_handlerFunc = handlerFunc;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _handlerFunc(request, cancellationToken);
}
}
EDIT2:一个最小的、可重现的例子——这需要以下包——Microsoft.AspNet.WebApi.Core、Microsoft.Extensions.Http、最小起订量:
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using Moq;
namespace StreamReaderEncoding
{
internal class Program
{
static Mock<IHttpClientFactory> mockHttpClientFactory;
static void Main(string[] args)
{
MainAsync().Wait();
}
static async Task MainAsync()
{
mockHttpClientFactory = new Mock<IHttpClientFactory>();
string content = @"{
""test"": ""true""
}";
Console.WriteLine($"content before: {content}");
HttpClient httpClient = SetupHttpClient((HttpRequestMessage request) =>
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(content);
writer.Flush();
stream.Position = 0;
StreamReader sr = new StreamReader(stream, true);
var response = request.CreateResponse(HttpStatusCode.OK, sr.ReadToEnd());
response.Content.Headers.ContentEncoding.Add("UTF-8");
return Task.FromResult(response);
});
HttpResponseMessage response = await httpClient.GetAsync("https://www.test.com");
Stream responseStream = await response.Content.ReadAsStreamAsync();
StreamReader responseReader = null;
if (response.Content.Headers.ContentEncoding.Count > 0)
responseReader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding(response.Content.Headers.ContentEncoding.First()));
else
responseReader = new StreamReader(responseStream, true);
content = await responseReader.ReadToEndAsync();
Console.WriteLine($"content after: {content}");
}
static HttpClient SetupHttpClient(Func<HttpRequestMessage, Task<HttpResponseMessage>> response)
{
var configuration = new HttpConfiguration();
var clientHandlerStub = new HttpDelegatingHandlerStub((request, cancellationToken) =>
{
request.SetConfiguration(configuration);
return response(request);
});
HttpClient httpClient = new HttpClient(clientHandlerStub);
mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);
return httpClient;
}
}
internal class HttpDelegatingHandlerStub : DelegatingHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
public HttpDelegatingHandlerStub()
{
_handlerFunc = (request, cancellationToken) => Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
}
public HttpDelegatingHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc)
{
_handlerFunc = handlerFunc;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _handlerFunc(request, cancellationToken);
}
}
}
示例的输出:
content before: {
"test": "true"
}
content after: "{\r\n \"test\": \"true\"\r\n}"
这与起订量无关...完全是关于 HttpRequestMessageExtensions.CreateResponse()
,它将您的字符串编码为 JSON。
这是一个更简单的示例(作为 .NET 6 控制台应用程序;您可以忍受它抱怨恢复 net461 的目标,或者将其重新定位为 .NET 4.7.1 或类似版本并添加一些项目选项和 using 指令;我认为让它以 .NET 6 为目标更简单。)
using System.Web.Http;
var request = new HttpRequestMessage();
request.SetConfiguration(new HttpConfiguration());
string json = "{ \"test\": \"true\" }";
Console.WriteLine($"Before: {json}");
var response = request.CreateResponse(json);
string text = await response.Content.ReadAsStringAsync();
Console.WriteLine($"After: {text}");
输出:
Before: { "test": "true" }
After: "{ \"test\": \"true\" }"
我认为让您感到困惑的是,在调试器中,您正在查看 response.Content
,将 ObjectContent<string>
中的 Value
视为字符串 如您所愿,并假设这是要写入响应的数据。它不是。这是在格式化之前的数据。
解决此问题的最简单方法是将响应内容提供为 StringContent
。至此你不需要 any 依赖 - 下面的代码是一个最小的例子,它为“之前”和“之后”打印相同的文本:
var request = new HttpRequestMessage();
string json = "{ \"test\": \"true\" }";
Console.WriteLine($"Before: {json}");
var response = new HttpResponseMessage { Content = new StringContent(json) };
string text = await response.Content.ReadAsStringAsync();
Console.WriteLine($"After: {text}");
当然你可能想在响应中设置一些其他的 headers,但我相信这证明它确实是 CreateResponse
方法(及其对 ObjectContent
的使用) 导致了问题。