Skip to content

Commit 7d6db13

Browse files
committed
Remove HttpRequestTracer.LastRequest since it's not thread-safe; simplify formatting APIs
1 parent 804e8bb commit 7d6db13

9 files changed

Lines changed: 128 additions & 133 deletions

src/Dibix.Http.Client/Client/DefaultHttpClientFactory.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -335,26 +335,23 @@ public IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(Func<THan
335335

336336
public void ConfigureDefaults()
337337
{
338-
// Ensure the tracing handler is ran before all other handlers
339-
ICollection<Action<HttpMessageHandlerBuilder>> httpMessageHandlerBuilderActions = this.HttpMessageHandlerBuilderActions.ToArray();
340-
this.HttpMessageHandlerBuilderActions.Clear();
341-
342-
if (this.Tracer != null)
343-
this.AddHttpMessageHandler(() => new TracingHttpMessageHandler(this.Tracer));
344-
345-
this.HttpMessageHandlerBuilderActions.AddRange(httpMessageHandlerBuilderActions);
346-
347-
if (this.WrapTimeoutsInException)
348-
this.AddHttpMessageHandler<TimeoutHttpMessageHandler>();
349-
350338
if (this.FollowRedirectsGetRequests)
351339
this.AddHttpMessageHandler<FollowRedirectHttpMessageHandler>();
352340

353341
if (this.TraceProxy)
354342
this.AddHttpMessageHandler<TraceProxyHttpMessageHandler>();
355343

344+
// Run tracing after all other handlers, that potentially modified the request, to ensure the trace includes the actual request that is sent.
345+
if (this.Tracer != null)
346+
this.AddHttpMessageHandler(() => new TracingHttpMessageHandler(this.Tracer));
347+
348+
// Add this after each handler, that needs to access the response, before an exception is thrown. (i.E. TracingHttpMessageHandler)
356349
if (this.EnsureSuccessStatusCode)
357350
this.AddHttpMessageHandler<EnsureSuccessStatusCodeHttpMessageHandler>();
351+
352+
// This should be as close to the primary handler as possible to avoid timeouts caused by other handlers.
353+
if (this.WrapTimeoutsInException)
354+
this.AddHttpMessageHandler<TimeoutHttpMessageHandler>();
358355
}
359356
}
360357

src/Dibix.Http.Client/Client/Handlers/TracingHttpMessageHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
4747
#endregion
4848

4949
#region Private Methods
50-
private Task TraceRequest(HttpRequestMessage request) => this._tracer.TraceRequestMessageAsync(request);
50+
private Task TraceRequest(HttpRequestMessage request) => this._tracer.TraceRequestAsync(request);
5151

52-
private Task TraceResponse(HttpResponseMessage response) => this._tracer.TraceResponseMessageAsync(response, this._requestDurationTracker.Elapsed);
52+
private Task TraceResponse(HttpResponseMessage response) => this._tracer.TraceResponseAsync(response, this._requestDurationTracker.Elapsed);
5353

5454
private void StartTracking() => this._requestDurationTracker.Restart();
5555

src/Dibix.Http.Client/Client/HttpException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class HttpException : Exception
1313
public HttpResponseMessage Response { get; }
1414
public string ResponseContentText { get; }
1515

16-
protected internal HttpException(HttpRequestMessage request, string requestContentText, HttpResponseMessage response, string responseContentText) : base(CreateMessage(response))
16+
private protected HttpException(HttpRequestMessage request, string requestContentText, HttpResponseMessage response, string responseContentText) : base(CreateMessage(response))
1717
{
1818
this.Request = request;
1919
this.RequestContentText = requestContentText;

src/Dibix.Http.Client/Client/HttpRequestExceptionExtensions.cs renamed to src/Dibix.Http.Client/Client/HttpExceptionExtensions.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
using Newtonsoft.Json;
1+
using System;
2+
using Newtonsoft.Json;
23

34
namespace Dibix.Http.Client
45
{
5-
public static class HttpRequestExceptionExtensions
6+
public static class HttpExceptionExtensions
67
{
78
public static T ReadContentAsJson<T>(this HttpException exception)
89
{
910
Guard.IsNotNull(exception, nameof(exception));
1011
return JsonConvert.DeserializeObject<T>(exception.ResponseContentText);
1112
}
1213

13-
public static string Format(this HttpException exception)
14+
public static string GetFormattedText(this HttpException exception, int? maxContentLength = null, bool maskSensitiveData = true)
1415
{
15-
string formattedRequest = HttpMessageFormatter.Format(exception.Request, exception.RequestContentText, maskSensitiveData: true);
16-
string formattedResponse = HttpMessageFormatter.Format(exception.Response, exception.ResponseContentText);
17-
return HttpMessageFormatter.Format(formattedRequest, formattedResponse);
16+
string formattedRequest = HttpMessageFormatter.Format(exception.Request, exception.RequestContentText, maskSensitiveData, maxContentLength);
17+
string formattedResponse = HttpMessageFormatter.Format(exception.Response, exception.ResponseContentText, maxContentLength);
18+
string text = $@"Request
19+
-------
20+
{formattedRequest}
21+
22+
Response
23+
--------
24+
{formattedResponse}";
25+
return text;
1826
}
1927
}
20-
}
28+
}

src/Dibix.Http.Client/Client/HttpMessageFormatter.cs

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,14 @@
1010

1111
namespace Dibix.Http.Client
1212
{
13-
public static class HttpMessageFormatter
13+
internal static class HttpMessageFormatter
1414
{
1515
#region Fields
1616
private static readonly Func<HttpHeaders, IEnumerable<KeyValuePair<string, string>>> GetHeaderStringsAccessor = CompileGetHeaderStrings();
1717
#endregion
1818

1919
#region Public Methods
20-
public static string Format(string formattedRequestText, string formattedResponseText, int? maxLength = null)
21-
{
22-
string formattedRequestTextTrimmed = formattedRequestText;
23-
string formattedResponseTextTrimmed = formattedResponseText;
24-
25-
if (maxLength.HasValue)
26-
{
27-
formattedRequestTextTrimmed = formattedRequestTextTrimmed.TrimIfNecessary(maxLength.Value);
28-
formattedResponseTextTrimmed = formattedResponseTextTrimmed.TrimIfNecessary(maxLength.Value);
29-
}
30-
31-
return $@"Request
32-
-------
33-
{formattedRequestTextTrimmed}
34-
35-
Response
36-
--------
37-
{formattedResponseTextTrimmed}";
38-
}
39-
#endregion
40-
41-
#region Internal Methods
42-
internal static string Format(HttpRequestMessage requestMessage, string requestContentText, bool maskSensitiveData)
20+
public static string Format(HttpRequestMessage requestMessage, string requestContentText, bool maskSensitiveData, int? maxContentLength)
4321
{
4422
Guard.IsNotNull(requestMessage, nameof(requestMessage));
4523

@@ -59,20 +37,23 @@ internal static string Format(HttpRequestMessage requestMessage, string requestC
5937

6038
if (requestContentText != null)
6139
{
62-
string secureRequestContentText = requestContentText;
40+
string normalizedRequestContentText = requestContentText;
6341
if (maskSensitiveData)
64-
secureRequestContentText = Regex.Replace(secureRequestContentText, "password=[^&]+", "password=*****");
42+
normalizedRequestContentText = Regex.Replace(normalizedRequestContentText, "password=[^&]+", "password=*****");
43+
44+
if (maxContentLength.HasValue)
45+
normalizedRequestContentText = normalizedRequestContentText.TrimIfNecessary(maxContentLength.Value);
6546

6647
sb.AppendLine()
6748
.AppendLine()
68-
.Append(secureRequestContentText);
49+
.Append(normalizedRequestContentText);
6950
}
7051

7152
string formattedRequest = sb.ToString();
7253
return formattedRequest;
7354
}
7455

75-
internal static string Format(HttpResponseMessage responseMessage, string responseContentText)
56+
public static string Format(HttpResponseMessage responseMessage, string responseContentText, int? maxContentLength)
7657
{
7758
Guard.IsNotNull(responseMessage, nameof(responseMessage));
7859

@@ -92,9 +73,13 @@ internal static string Format(HttpResponseMessage responseMessage, string respon
9273

9374
if (responseContentText.Length > 0)
9475
{
76+
string normalizedResponseContentText = responseContentText;
77+
if (maxContentLength.HasValue)
78+
normalizedResponseContentText = normalizedResponseContentText.TrimIfNecessary(maxContentLength.Value);
79+
9580
sb.AppendLine()
9681
.AppendLine()
97-
.Append(responseContentText);
82+
.Append(normalizedResponseContentText);
9883
}
9984

10085
string formattedResponse = sb.ToString();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Net.Http;
2+
3+
namespace Dibix.Http.Client
4+
{
5+
public static class HttpRequestMessageExtensions
6+
{
7+
public static string GetFormattedText(this HttpRequestMessage requestMessage, string requestContentText, int? maxContentLength = null, bool maskSensitiveData = true)
8+
{
9+
return HttpMessageFormatter.Format(requestMessage, requestContentText, maskSensitiveData, maxContentLength);
10+
}
11+
12+
public static string GetFormattedText(this HttpResponseMessage responseMessage, string responseContentText, int? maxContentLength = null)
13+
{
14+
return HttpMessageFormatter.Format(responseMessage, responseContentText, maxContentLength);
15+
}
16+
}
17+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Net.Http;
3+
4+
namespace Dibix.Http.Client
5+
{
6+
internal static class HttpRequestPropertyExtensions
7+
{
8+
private const string PropertyKeyHttpRequestTrace = "Dibix.Http.Client.HttpRequestTrace";
9+
10+
public static bool TryGetHttpRequestTrace(this HttpRequestMessage requestMessage, out HttpRequestTrace requestTrace) => HttpRequestPropertyExtensions.TryGetPropertyValue(requestMessage, PropertyKeyHttpRequestTrace, out requestTrace);
11+
12+
public static HttpRequestTrace GetHttpRequestTrace(this HttpRequestMessage requestMessage) => HttpRequestPropertyExtensions.GetRequestProperty<HttpRequestTrace>(requestMessage, PropertyKeyHttpRequestTrace);
13+
14+
public static void SetHttpRequestTrace(this HttpRequestMessage requestMessage, HttpRequestTrace requestTrace) => HttpRequestPropertyExtensions.SetRequestProperty(requestMessage, PropertyKeyHttpRequestTrace, requestTrace);
15+
16+
private static T GetRequestProperty<T>(HttpRequestMessage requestMessage, string propertyName)
17+
{
18+
if (!TryGetPropertyValue(requestMessage, propertyName, out T value))
19+
throw new InvalidOperationException(FormattableString.Invariant($"Missing property '{propertyName}' on HTTP request message"));
20+
21+
return value;
22+
}
23+
24+
private static void SetRequestProperty<T>(HttpRequestMessage requestMessage, string propertyName, T value)
25+
{
26+
#if NETCOREAPP
27+
requestMessage.Options.Set(new HttpRequestOptionsKey<T>(propertyName), value);
28+
#else
29+
requestMessage.Properties[propertyName] = value;
30+
#endif
31+
}
32+
33+
private static bool TryGetPropertyValue<T>(HttpRequestMessage requestMessage, string propertyName, out T value)
34+
{
35+
#if NETCOREAPP
36+
return requestMessage.Options.TryGetValue(new HttpRequestOptionsKey<T>(propertyName), out value);
37+
#else
38+
if (requestMessage.Properties.TryGetValue(propertyName, out object rawValue))
39+
{
40+
value = (T)rawValue;
41+
return true;
42+
}
43+
value = default;
44+
return false;
45+
#endif
46+
}
47+
}
48+
}
Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
11
using System;
2-
using System.Net.Http;
32

43
namespace Dibix.Http.Client
54
{
6-
public sealed class HttpRequestTrace
5+
public class HttpRequestTrace
76
{
8-
public HttpRequestMessage RequestMessage { get; }
9-
public string FormattedRequestText { get; set; }
10-
public HttpResponseMessage ResponseMessage { get; set; }
11-
public string FormattedResponseText { get; set; }
127
public TimeSpan Duration { get; set; }
13-
14-
internal HttpRequestTrace(HttpRequestMessage requestMessage, string formattedRequestText)
15-
{
16-
this.RequestMessage = requestMessage;
17-
this.FormattedRequestText = formattedRequestText;
18-
}
198
}
209
}

src/Dibix.Http.Client/Client/HttpRequestTracer.cs

Lines changed: 21 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,98 +7,49 @@ namespace Dibix.Http.Client
77
public class HttpRequestTracer
88
{
99
public bool MaskSensitiveContent { get; } = true;
10-
public HttpRequestTrace LastRequest { get; private set; }
1110

1211
public HttpRequestTracer() { }
1312
public HttpRequestTracer(bool maskSensitiveContent)
1413
{
1514
this.MaskSensitiveContent = maskSensitiveContent;
1615
}
1716

18-
internal async Task TraceRequestMessageAsync(HttpRequestMessage requestMessage)
17+
internal async Task TraceRequestAsync(HttpRequestMessage requestMessage)
1918
{
20-
await this.CollectLastRequest(requestMessage).ConfigureAwait(false);
21-
await this.TraceRequestAsync(requestMessage).ConfigureAwait(false);
19+
HttpRequestTrace requestTrace = CreateRequestTrace();
20+
requestMessage.SetHttpRequestTrace(requestTrace);
21+
await this.TraceRequestAsync(requestMessage, requestTrace).ConfigureAwait(false);
2222
}
2323

24-
internal async Task TraceResponseMessageAsync(HttpResponseMessage responseMessage, TimeSpan duration)
24+
internal async Task TraceResponseAsync(HttpResponseMessage responseMessage, TimeSpan duration)
2525
{
26-
await CompleteLastRequest(responseMessage, duration).ConfigureAwait(false);
27-
await this.TraceResponseAsync(responseMessage, duration);
26+
HttpRequestTrace requestTrace = responseMessage.RequestMessage.GetHttpRequestTrace();
27+
CompleteLastRequest(requestTrace, duration);
28+
await this.TraceResponseAsync(responseMessage, requestTrace);
2829
}
2930

30-
protected virtual Task TraceRequestAsync(HttpRequestMessage requestMessage) => Task.CompletedTask;
31+
protected virtual Task TraceRequestAsync(HttpRequestMessage requestMessage, HttpRequestTrace requestTrace) => Task.CompletedTask;
3132

32-
protected virtual Task TraceResponseAsync(HttpResponseMessage responseMessage, TimeSpan duration) => Task.CompletedTask;
33+
protected virtual Task TraceResponseAsync(HttpResponseMessage responseMessage, HttpRequestTrace requestTrace) => Task.CompletedTask;
3334

34-
protected virtual bool ShouldBufferRequestContent(HttpRequestMessage requestMessage) => false;
35+
private protected virtual HttpRequestTrace CreateRequestTrace() => new HttpRequestTrace();
3536

36-
protected virtual bool ShouldBufferResponseContent(HttpRequestMessage requestMessage) => false;
37-
38-
private async Task CollectLastRequest(HttpRequestMessage requestMessage)
39-
{
40-
string formattedRequestText = await this.GetFormattedRequestContent(requestMessage).ConfigureAwait(false);
41-
this.LastRequest = new HttpRequestTrace(requestMessage, formattedRequestText);
42-
}
43-
44-
private async Task CompleteLastRequest(HttpResponseMessage responseMessage, TimeSpan duration)
45-
{
46-
if (this.LastRequest == null)
47-
throw new InvalidOperationException("Request not initialized");
48-
49-
string formattedResponseText = await this.GetFormattedResponseContent(responseMessage).ConfigureAwait(false);
50-
this.LastRequest.ResponseMessage = responseMessage;
51-
this.LastRequest.FormattedResponseText = formattedResponseText;
52-
this.LastRequest.Duration = duration;
53-
54-
// Since non successful status code will throw an exception,
55-
// it is now safe to restore the request content, that was not previously captured
56-
HttpRequestMessage requestMessage = responseMessage.RequestMessage;
57-
if (!this.ShouldBufferRequestContent(requestMessage) && !responseMessage.IsSuccessStatusCode)
58-
{
59-
string requestContentText = await GetRequestContentTextAsync(requestMessage, bufferRequestContent: true).ConfigureAwait(false);
60-
this.LastRequest.FormattedRequestText = this.FormatRequest(requestMessage, requestContentText);
61-
}
62-
}
63-
64-
private async Task<string> GetFormattedRequestContent(HttpRequestMessage requestMessage)
65-
{
66-
bool bufferRequestContent = this.ShouldBufferRequestContent(requestMessage);
67-
string requestContentText = await GetRequestContentTextAsync(requestMessage, bufferRequestContent).ConfigureAwait(false);
68-
string formattedRequestText = this.FormatRequest(requestMessage, requestContentText);
69-
return formattedRequestText;
70-
}
71-
72-
private async Task<string> GetFormattedResponseContent(HttpResponseMessage responseMessage)
37+
private static void CompleteLastRequest(HttpRequestTrace requestTrace, TimeSpan duration)
7338
{
74-
bool bufferResponseContent = this.ShouldBufferResponseContent(responseMessage.RequestMessage) || !responseMessage.IsSuccessStatusCode;
75-
string responseContentTest = await GetResponseContentTextAsync(responseMessage, bufferResponseContent).ConfigureAwait(false);
76-
string formattedResponseText = HttpMessageFormatter.Format(responseMessage, responseContentTest);
77-
return formattedResponseText;
39+
requestTrace.Duration = duration;
7840
}
41+
}
7942

80-
private string FormatRequest(HttpRequestMessage requestMessage, string requestContentText) => HttpMessageFormatter.Format(requestMessage, requestContentText, this.MaskSensitiveContent);
81-
82-
private static async Task<string> GetRequestContentTextAsync(HttpRequestMessage requestMessage, bool bufferRequestContent)
83-
{
84-
if (requestMessage.Content == null)
85-
return null;
86-
87-
if (!bufferRequestContent)
88-
return "<Request content unavailable>";
43+
public class HttpRequestTracer<T> : HttpRequestTracer where T : HttpRequestTrace, new()
44+
{
45+
protected sealed override Task TraceRequestAsync(HttpRequestMessage requestMessage, HttpRequestTrace requestTrace) => this.TraceRequestAsync(requestMessage, (T)requestTrace);
8946

90-
return await requestMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
91-
}
47+
protected sealed override Task TraceResponseAsync(HttpResponseMessage responseMessage, HttpRequestTrace requestTrace) => this.TraceResponseAsync(responseMessage, (T)requestTrace);
9248

93-
private static async Task<string> GetResponseContentTextAsync(HttpResponseMessage responseMessage, bool bufferResponseContent)
94-
{
95-
if (responseMessage.Content == null)
96-
return null;
49+
private protected sealed override HttpRequestTrace CreateRequestTrace() => new T();
9750

98-
if (!bufferResponseContent)
99-
return "<Response content unavailable>";
51+
protected virtual Task TraceRequestAsync(HttpRequestMessage requestMessage, T requestTrace) => Task.CompletedTask;
10052

101-
return await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
102-
}
53+
protected virtual Task TraceResponseAsync(HttpResponseMessage responseMessage, T requestTrace) => Task.CompletedTask;
10354
}
10455
}

0 commit comments

Comments
 (0)