Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support synchronous HttpClient.Send in SentryHttpMessageHandler #2336

Merged
merged 6 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Restore `System.Reflection.Metadata` dependency for .NET Core 3 ([#2302](https://github.com/getsentry/sentry-dotnet/pull/2302))
- Capture open transactions on disabled hubs ([#2319](https://github.com/getsentry/sentry-dotnet/pull/2319))
- Remove session breadcrumbs ([#2333](https://github.com/getsentry/sentry-dotnet/pull/2333))
- Support synchronous `HttpClient.Send` in `SentryHttpMessageHandler` ([#2336](https://github.com/getsentry/sentry-dotnet/pull/2336))

### Dependencies

Expand Down
140 changes: 86 additions & 54 deletions src/Sentry/SentryHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,97 @@ public class SentryHttpMessageHandler : DelegatingHandler
private readonly SentryOptions? _options;

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// Constructs an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
public SentryHttpMessageHandler(IHub hub)
{
_hub = hub;
_options = hub.GetSentryOptions();
}
public SentryHttpMessageHandler()
: this(default, default, default) { }

/// <summary>
/// Constructs an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
/// <param name="innerHandler">An inner message handler to delegate calls to.</param>
public SentryHttpMessageHandler(HttpMessageHandler innerHandler)
: this(default, default, innerHandler) { }

internal SentryHttpMessageHandler(IHub hub, SentryOptions options)
/// <summary>
/// Constructs an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
/// <param name="hub">The Sentry hub.</param>
public SentryHttpMessageHandler(IHub hub)
: this(hub, default)
{
_hub = hub;
_options = options;
}

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// Constructs an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
/// <param name="innerHandler">An inner message handler to delegate calls to.</param>
/// <param name="hub">The Sentry hub.</param>
public SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub)
: this(hub)
: this(hub, default, innerHandler)
{
InnerHandler = innerHandler;
}

internal SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub, SentryOptions options)
: this(hub, options)
internal SentryHttpMessageHandler(IHub? hub, SentryOptions? options, HttpMessageHandler? innerHandler = default)
{
InnerHandler = innerHandler;
}

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
public SentryHttpMessageHandler(HttpMessageHandler innerHandler)
: this(innerHandler, HubAdapter.Instance) { }
_hub = hub ?? HubAdapter.Instance;
_options = options ?? _hub.GetSentryOptions();

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
public SentryHttpMessageHandler()
: this(HubAdapter.Instance) { }
// Only assign the inner handler if it is supplied. We can't assign null or it will throw.
// We also cannot assign a default value here, or it will throw when used with HttpMessageHandlerBuilderFilter.
if (innerHandler is not null)
{
InnerHandler = innerHandler;
}
}

/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Prevent null reference exception in the following call
// in case the user didn't set an inner handler.
var (span, method, url) = ProcessRequest(request);

try
{
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
HandleResponse(response, span, method, url);
return response;
}
catch (Exception ex)
{
span?.Finish(ex);
throw;
}
}

#if NET5_0_OR_GREATER
/// <inheritdoc />
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
var (span, method, url) = ProcessRequest(request);

try
{
var response = base.Send(request, cancellationToken);
HandleResponse(response, span, method, url);
return response;
}
catch (Exception ex)
{
span?.Finish(ex);
throw;
}
}
#endif

private (ISpan? Span, string Method, string Url) ProcessRequest(HttpRequestMessage request)
{
// Assign a default inner handler for convenience the first time this is used.
// We can't do this in a constructor, or it will throw when used with HttpMessageHandlerBuilderFilter.
InnerHandler ??= new HttpClientHandler();

var requestMethod = request.Method.Method.ToUpperInvariant();
var method = request.Method.Method.ToUpperInvariant();
var url = request.RequestUri?.ToString() ?? string.Empty;

if (_options?.TracePropagationTargets.ShouldPropagateTrace(url) is true or null)
Expand All @@ -73,33 +113,25 @@ protected override async Task<HttpResponseMessage> SendAsync(

// Start a span that tracks this request
// (may be null if transaction is not set on the scope)
var span = _hub.GetSpan()?.StartChild(
"http.client",
// e.g. "GET https://example.com"
$"{requestMethod} {url}");

try
{
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

var breadcrumbData = new Dictionary<string, string>
{
{ "url", url },
{ "method", requestMethod },
{ "status_code", ((int)response.StatusCode).ToString() }
};
_hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData);
// e.g. "GET https://example.com"
var span = _hub.GetSpan()?.StartChild("http.client", $"{method} {url}");

// This will handle unsuccessful status codes as well
span?.Finish(SpanStatusConverter.FromHttpStatusCode(response.StatusCode));
return (span, method, url);
}

return response;
}
catch (Exception ex)
private void HandleResponse(HttpResponseMessage response, ISpan? span, string method, string url)
{
var breadcrumbData = new Dictionary<string, string>
{
span?.Finish(ex);
throw;
}
{"url", url},
{"method", method},
{"status_code", ((int) response.StatusCode).ToString()}
};
_hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData);

// This will handle unsuccessful status codes as well
var status = SpanStatusConverter.FromHttpStatusCode(response.StatusCode);
span?.Finish(status);
}

private void AddSentryTraceHeader(HttpRequestMessage request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ private class RecordingHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next) =>
handlerBuilder =>
{
// Set the fake handler to prevent outgoing HTTP requests in this test.
handlerBuilder.PrimaryHandler = new FakeHttpMessageHandler();
handlerBuilder.AdditionalHandlers.Add(_handler);
next(handlerBuilder);
};
Expand All @@ -34,7 +36,7 @@ public async Task Generated_client_sends_Sentry_trace_header_automatically()
// Will use this to record outgoing requests
using var recorder = new RecordingHttpMessageHandler();

var hub = new Internal.Hub(new SentryOptions
var hub = new Hub(new SentryOptions
{
Dsn = ValidDsn
});
Expand Down Expand Up @@ -65,11 +67,6 @@ public async Task Generated_client_sends_Sentry_trace_header_automatically()
.GetRequiredService<IHttpClientFactory>()
.CreateClient();

// The framework setup pipeline would end up adding an HttpClientHandler and this test
// would require access to the Internet. So overriding it here
// so the request stops at our stub:
recorder.InnerHandler = new FakeHttpMessageHandler();

await httpClient.GetAsync("https://fake.tld");
});
});
Expand Down
16 changes: 7 additions & 9 deletions test/Sentry.Testing/FakeHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ public FakeHttpMessageHandler(Func<HttpResponseMessage> getResponse)

public FakeHttpMessageHandler() { }

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return Task.FromResult(
_getResponse is not null
? _getResponse(request)
: new HttpResponseMessage(HttpStatusCode.OK));
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
Task.FromResult(_getResponse?.Invoke(request) ?? new HttpResponseMessage(HttpStatusCode.OK));

#if NET5_0_OR_GREATER
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
_getResponse?.Invoke(request) ?? new HttpResponseMessage(HttpStatusCode.OK);
#endif
}
46 changes: 45 additions & 1 deletion test/Sentry.Testing/HttpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public static IEnumerable<HttpMessageHandler> GetMessageHandlers(this HttpClient
} while (handler != null);
}

public static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage source)
public static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage source,
CancellationToken cancellationToken)
{
var clone = new HttpRequestMessage(source.Method, source.RequestUri) { Version = source.Version };

Expand All @@ -38,7 +39,11 @@ public static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage
{
var cloneContentStream = new MemoryStream();

#if NET5_0_OR_GREATER
await source.Content.CopyToAsync(cloneContentStream, cancellationToken).ConfigureAwait(false);
#else
await source.Content.CopyToAsync(cloneContentStream).ConfigureAwait(false);
#endif
cloneContentStream.Position = 0;

clone.Content = new StreamContent(cloneContentStream);
Expand All @@ -52,4 +57,43 @@ public static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage

return clone;
}

#if NET5_0_OR_GREATER
public static HttpRequestMessage Clone(this HttpRequestMessage source, CancellationToken cancellationToken)
{
var clone = new HttpRequestMessage(source.Method, source.RequestUri) { Version = source.Version };

// Headers
foreach (var (key, value) in source.Headers)
{
clone.Headers.TryAddWithoutValidation(key, value);
}

// Content
if (source.Content != null)
{
var cloneContentStream = new MemoryStream();

source.Content.CopyTo(cloneContentStream, default, cancellationToken);
cloneContentStream.Position = 0;

clone.Content = new StreamContent(cloneContentStream);

// Content headers
foreach (var (key, value) in source.Content.Headers)
{
clone.Content.Headers.Add(key, value);
}
}

return clone;
}

public static HttpResponseMessage Get(this HttpClient client, string requestUri,
CancellationToken cancellationToken = default)
{
var message = new HttpRequestMessage(HttpMethod.Get, requestUri);
return client.Send(message, cancellationToken);
}
#endif
}
21 changes: 11 additions & 10 deletions test/Sentry.Testing/RecordingHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ public class RecordingHttpMessageHandler : DelegatingHandler

public RecordingHttpMessageHandler() { }

public RecordingHttpMessageHandler(HttpMessageHandler innerHandler) =>
InnerHandler = innerHandler;
public RecordingHttpMessageHandler(HttpMessageHandler innerHandler) => InnerHandler = innerHandler;

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Clone the request to avoid ObjectDisposedException
_requests.Add(await request.CloneAsync());

InnerHandler ??= new FakeHttpMessageHandler();

_requests.Add(await request.CloneAsync(cancellationToken));
return await base.SendAsync(request, cancellationToken);
}

#if NET5_0_OR_GREATER
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
_requests.Add(request.Clone(cancellationToken));
return base.Send(request, cancellationToken);
}
#endif

public IReadOnlyList<HttpRequestMessage> GetRequests() => _requests.ToArray();

protected override void Dispose(bool disposing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ namespace Sentry
public SentryHttpMessageHandler(Sentry.IHub hub) { }
public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { }
public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { }
protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
protected override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
}
public readonly struct SentryId : Sentry.IJsonSerializable, System.IEquatable<Sentry.SentryId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ namespace Sentry
public SentryHttpMessageHandler(Sentry.IHub hub) { }
public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { }
public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { }
protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
protected override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
}
public readonly struct SentryId : Sentry.IJsonSerializable, System.IEquatable<Sentry.SentryId>
Expand Down
Loading