Skip to content

Commit be9c8fe

Browse files
committed
Copy some code from HttpTracer to avoid unsigned assembly loading issue
1 parent 0c5bb20 commit be9c8fe

File tree

7 files changed

+278
-12
lines changed

7 files changed

+278
-12
lines changed

Directory.Packages.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
<PackageVersion Include="AutoFixture" Version="4.18.1" />
3737
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
3838
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
39-
<PackageVersion Include="HttpTracer" Version="2.1.1" />
4039
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftTestHostVer)" />
4140
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
4241
<PackageVersion Include="Moq" Version="4.20.72" />

test/RestSharp.Tests.Integrated/Fixtures/OutputLogger.cs

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
namespace RestSharp.Tests.Integrated.HttpTracer;
3+
4+
[Flags]
5+
public enum HttpMessageParts {
6+
Unspecified = 0,
7+
RequestBody = 1 << 1,
8+
RequestHeaders = 1 << 2,
9+
ResponseBody = 1 << 3,
10+
ResponseHeaders = 1 << 4,
11+
RequestCookies = 1 << 5,
12+
13+
RequestAll = RequestBody | RequestHeaders | RequestCookies,
14+
ResponseAll = ResponseBody | ResponseHeaders,
15+
All = ResponseAll | RequestAll
16+
}
17+
18+
[Flags]
19+
public enum JsonFormatting {
20+
None = 0,
21+
IndentRequest = 1 << 0,
22+
IndentResponse = 1 << 1
23+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
using System.Diagnostics;
2+
using System.Net.Http.Headers;
3+
using System.Text;
4+
5+
namespace RestSharp.Tests.Integrated.HttpTracer;
6+
7+
public sealed class HttpTracerHandler : DelegatingHandler {
8+
static HttpMessageParts DefaultVerbosity => HttpMessageParts.All;
9+
10+
static string DefaultDurationFormat => "Duration: {0:ss\\:fffffff}";
11+
12+
static string LogMessageIndicatorPrefix => MessageIndicator;
13+
14+
static string LogMessageIndicatorSuffix => MessageIndicator;
15+
16+
/// <summary>
17+
/// Instance verbosity bitmask, setting the instance verbosity overrides <see cref="DefaultVerbosity"/> <see cref="HttpMessageParts"/>
18+
/// </summary>
19+
HttpMessageParts Verbosity {
20+
get => field == HttpMessageParts.Unspecified ? DefaultVerbosity : field;
21+
init;
22+
}
23+
24+
JsonFormatting JsonFormatting => JsonFormatting.None;
25+
26+
/// <summary> Constructs the <see cref="HttpTracerHandler"/> with a custom <see cref="IHttpTracerLogger"/> and a custom <see cref="HttpMessageHandler"/></summary>
27+
/// <param name="handler">User defined <see cref="HttpMessageHandler"/></param>
28+
/// <param name="logger">User defined <see cref="IHttpTracerLogger"/></param>
29+
/// <param name="verbosity">Instance verbosity bitmask, setting the instance verbosity overrides <see cref="DefaultVerbosity"/> <see cref="HttpMessageParts"/></param>
30+
public HttpTracerHandler(HttpMessageHandler? handler, IHttpTracerLogger logger, HttpMessageParts verbosity = HttpMessageParts.Unspecified) {
31+
InnerHandler = handler ??
32+
new HttpClientHandler {
33+
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
34+
};
35+
_logger = logger;
36+
Verbosity = verbosity;
37+
}
38+
39+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
40+
try {
41+
await LogHttpRequest(request).ConfigureAwait(false);
42+
43+
var stopwatch = new Stopwatch();
44+
stopwatch.Start();
45+
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
46+
stopwatch.Stop();
47+
48+
await LogHttpResponse(response, stopwatch.Elapsed).ConfigureAwait(false);
49+
return response;
50+
}
51+
catch (Exception ex) {
52+
LogHttpException(request.Method, request.RequestUri, ex);
53+
throw;
54+
}
55+
}
56+
57+
static string ProcessRequestUri(Uri? uri) => uri?.ToString() ?? string.Empty;
58+
59+
static string ProcessResponseLogHeading(HttpStatusCode statusCode, bool isSuccessStatusCode) {
60+
const string succeeded = "SUCCEEDED";
61+
const string failed = "FAILED";
62+
63+
string responseResult;
64+
65+
if (statusCode == default) {
66+
responseResult = failed;
67+
}
68+
else {
69+
responseResult = isSuccessStatusCode
70+
? $"{succeeded}: {(int)statusCode} {statusCode}"
71+
: $"{failed}: {(int)statusCode} {statusCode}";
72+
}
73+
74+
return responseResult;
75+
}
76+
77+
static string ProcessRequestHeaders(HttpRequestHeaders requestHeaders) => $"{requestHeaders.ToString().TrimEnd().TrimEnd('}').TrimStart('{')}";
78+
79+
static string ProcessResponseHeaders(HttpResponseMessage? responseHeaders) => responseHeaders?.ToString() ?? string.Empty;
80+
81+
static string ProcessCookieHeader(CookieContainer cookieContainer, Uri requestRequestUri) => cookieContainer.GetCookieHeader(requestRequestUri);
82+
83+
static Task<string> ProcessRequestBody(HttpContent? requestContent) => requestContent?.ReadAsStringAsync() ?? Task.FromResult(string.Empty);
84+
85+
static Task<string> ProcessResponseBody(HttpContent? responseContent) => responseContent?.ReadAsStringAsync() ?? Task.FromResult(string.Empty);
86+
87+
async Task LogHttpRequest(HttpRequestMessage request) {
88+
var sb = new StringBuilder();
89+
90+
ConditionalAddRequestPrefix(request.Method, request.RequestUri, sb);
91+
ConditionalAddRequestHeaders(request.Headers, sb);
92+
ConditionalAddCookies(request.RequestUri, sb);
93+
await ConditionalAddRequestBody(request.Content, sb);
94+
95+
if (sb.Length > 0)
96+
_logger.Log(sb.ToString());
97+
}
98+
99+
async Task LogHttpResponse(HttpResponseMessage? response, TimeSpan duration) {
100+
var sb = new StringBuilder();
101+
102+
ConditionalAddResponsePrefix(
103+
response?.StatusCode,
104+
response?.IsSuccessStatusCode,
105+
response?.RequestMessage?.Method,
106+
response?.RequestMessage?.RequestUri,
107+
sb
108+
);
109+
ConditionalAddResponseHeaders(response, sb);
110+
await ConditionalAddResponseBody(response?.Content, sb);
111+
ConditionalAddResponsePostfix(duration, sb);
112+
113+
if (sb.Length > 0)
114+
_logger.Log(sb.ToString());
115+
}
116+
117+
void LogHttpException(HttpMethod requestMethod, Uri? requestUri, Exception ex) {
118+
var httpExceptionString = $"""
119+
{LogMessageIndicatorPrefix} HTTP EXCEPTION: [{requestMethod}]{LogMessageIndicatorSuffix}
120+
{requestMethod} {requestUri}
121+
{ex}
122+
""";
123+
_logger.Log(httpExceptionString);
124+
}
125+
126+
private void ConditionalAddResponsePostfix(TimeSpan duration, StringBuilder sb) {
127+
if (!Verbosity.HasFlag(HttpMessageParts.ResponseHeaders) && !Verbosity.HasFlag(HttpMessageParts.ResponseBody)) return;
128+
129+
var httpResponsePostfix = string.Format(DefaultDurationFormat, duration);
130+
sb.AppendLine(httpResponsePostfix);
131+
}
132+
133+
private async Task ConditionalAddResponseBody(HttpContent? responseContent, StringBuilder sb) {
134+
if (!Verbosity.HasFlag(HttpMessageParts.ResponseBody)) return;
135+
136+
var httpResponseContent = await ProcessResponseBody(responseContent).ConfigureAwait(false);
137+
138+
if (JsonFormatting.HasFlag(JsonFormatting.IndentResponse) && responseContent?.Headers.ContentType?.MediaType == JsonContentType) {
139+
httpResponseContent = PrettyFormatJson(httpResponseContent);
140+
}
141+
142+
sb.AppendLine(httpResponseContent);
143+
}
144+
145+
private void ConditionalAddResponseHeaders(HttpResponseMessage? responseHeaders, StringBuilder sb) {
146+
if (!Verbosity.HasFlag(HttpMessageParts.ResponseHeaders)) return;
147+
148+
var httpResponseHeaders = ProcessResponseHeaders(responseHeaders);
149+
sb.AppendLine(httpResponseHeaders);
150+
}
151+
152+
private void ConditionalAddResponsePrefix(
153+
HttpStatusCode? responseStatusCode,
154+
bool? responseIsSuccessStatusCode,
155+
HttpMethod? requestMessageMethod,
156+
Uri? requestMessageRequestUri,
157+
StringBuilder sb
158+
) {
159+
if (!Verbosity.HasFlag(HttpMessageParts.ResponseHeaders) && !Verbosity.HasFlag(HttpMessageParts.ResponseBody)) return;
160+
161+
var responseResult = ProcessResponseLogHeading(responseStatusCode ?? default, responseIsSuccessStatusCode ?? false);
162+
163+
var httpResponsePrefix = $"{LogMessageIndicatorPrefix}HTTP RESPONSE: [{responseResult}]{LogMessageIndicatorSuffix}";
164+
sb.AppendLine(httpResponsePrefix);
165+
166+
var httpRequestMethodUri = $"{requestMessageMethod} {ProcessRequestUri(requestMessageRequestUri)}";
167+
sb.AppendLine(httpRequestMethodUri);
168+
}
169+
170+
private async Task ConditionalAddRequestBody(HttpContent? requestContent, StringBuilder sb) {
171+
if (!Verbosity.HasFlag(HttpMessageParts.RequestBody)) return;
172+
173+
var httpRequestBody = await ProcessRequestBody(requestContent).ConfigureAwait(false);
174+
175+
if (JsonFormatting.HasFlag(JsonFormatting.IndentRequest) && requestContent?.Headers.ContentType?.MediaType == JsonContentType) {
176+
httpRequestBody = PrettyFormatJson(httpRequestBody);
177+
}
178+
179+
sb.AppendLine(httpRequestBody);
180+
}
181+
182+
private void ConditionalAddRequestHeaders(HttpRequestHeaders requestHeaders, StringBuilder sb) {
183+
if (!Verbosity.HasFlag(HttpMessageParts.RequestHeaders)) return;
184+
185+
var httpErrorRequestHeaders = ProcessRequestHeaders(requestHeaders);
186+
sb.AppendLine(httpErrorRequestHeaders);
187+
}
188+
189+
private void ConditionalAddRequestPrefix(HttpMethod requestMethod, Uri? requestUri, StringBuilder sb) {
190+
if (!Verbosity.HasFlag(HttpMessageParts.RequestHeaders) && !Verbosity.HasFlag(HttpMessageParts.RequestBody)) return;
191+
192+
var httpRequestPrefix = $"{LogMessageIndicatorPrefix}HTTP REQUEST: [{requestMethod}]{LogMessageIndicatorSuffix}";
193+
sb.AppendLine(httpRequestPrefix);
194+
195+
var httpRequestMethodUri = $"{requestMethod} {ProcessRequestUri(requestUri)}";
196+
sb.AppendLine(httpRequestMethodUri);
197+
}
198+
199+
private void ConditionalAddCookies(Uri? requestUri, StringBuilder sb) {
200+
if (!Verbosity.HasFlag(HttpMessageParts.RequestCookies) ||
201+
InnerHandler is not HttpClientHandler httpClientHandler) return;
202+
203+
var cookieHeader = ProcessCookieHeader(httpClientHandler.CookieContainer, requestUri ?? new Uri(""));
204+
if (string.IsNullOrWhiteSpace(cookieHeader)) return;
205+
206+
sb.AppendLine($"{Environment.NewLine}Cookie: {cookieHeader}");
207+
}
208+
209+
private static string PrettyFormatJson(string json) {
210+
var indentation = 0;
211+
var quoteCount = 0;
212+
213+
var result = json.Select(ch => new { ch, quotes = ch == '"' ? quoteCount++ : quoteCount })
214+
.Select(t => new {
215+
t,
216+
lineBreak = t.ch == ',' && t.quotes % 2 == 0
217+
? $"{t.ch}{Environment.NewLine}{string.Concat(Enumerable.Repeat(JsonIndentationString, indentation))}"
218+
: null
219+
}
220+
)
221+
.Select(t => new {
222+
t,
223+
openChar = t.t.ch is '{' or '['
224+
? $"{t.t.ch}{Environment.NewLine}{string.Concat(Enumerable.Repeat(JsonIndentationString, ++indentation))}"
225+
: t.t.ch.ToString()
226+
}
227+
)
228+
.Select(t => new {
229+
t,
230+
closeChar = t.t.t.ch is '}' or ']'
231+
? $"{Environment.NewLine}{string.Concat(Enumerable.Repeat(JsonIndentationString, --indentation))}{t.t.t.ch}"
232+
: t.t.t.ch.ToString()
233+
}
234+
)
235+
.Select(t => t.t.t.lineBreak ?? (t.t.openChar.Length > 1 ? t.t.openChar : t.closeChar));
236+
237+
return string.Concat(result);
238+
}
239+
240+
private readonly IHttpTracerLogger _logger;
241+
private const string JsonContentType = "application/json";
242+
private const string MessageIndicator = " ==================== ";
243+
private const string JsonIndentationString = " ";
244+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace RestSharp.Tests.Integrated.HttpTracer;
2+
3+
public interface IHttpTracerLogger {
4+
void Log(string message);
5+
}
6+
7+
public class OutputHttpTracerLogger(ITestOutputHelper output) : IHttpTracerLogger {
8+
public void Log(string message) => output.WriteLine(message);
9+
}

test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using HttpTracer;
2-
using RestSharp.Tests.Integrated.Fixtures;
1+
using RestSharp.Tests.Integrated.HttpTracer;
32
using RestSharp.Tests.Shared.Extensions;
43
using RestSharp.Tests.Shared.Fixtures;
54

@@ -15,7 +14,7 @@ public MultipartFormDataTests(ITestOutputHelper output) {
1514
_capturer = _server.ConfigureBodyCapturer(Method.Post);
1615

1716
var options = new RestClientOptions($"{_server.Url!}{RequestBodyCapturer.Resource}") {
18-
ConfigureMessageHandler = handler => new HttpTracerHandler(handler, new OutputLogger(output), HttpMessageParts.All)
17+
ConfigureMessageHandler = handler => new HttpTracerHandler(handler, new OutputHttpTracerLogger(output), HttpMessageParts.All)
1918
};
2019
_client = new RestClient(options);
2120
}

test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
</ItemGroup>
1616
<ItemGroup>
1717
<PackageReference Include="HttpMultipartParser"/>
18-
<PackageReference Include="HttpTracer"/>
1918
<PackageReference Include="Polly"/>
2019
<PackageReference Include="PolySharp" PrivateAssets="All"/>
2120
<PackageReference Include="WireMock.Net"/>

0 commit comments

Comments
 (0)