-
-
Save MihaZupan/8f28566fdc4b8e20491221ed84362fcf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var handler = new SocketsHttpHandler(); | |
HttpClientDiagnosticsHelper.ConfigureDebugConnectCallback(handler); | |
var client = new HttpClient(handler); | |
var request = new HttpRequestMessage(HttpMethod.Get, "https://httpbin.org/get"); | |
var (response, endPoint, ex) = await HttpClientDiagnosticsHelper.SendAsync(client, request, CancellationToken.None); | |
if (endPoint is not null) | |
{ | |
Console.WriteLine(endPoint); | |
} | |
public static class HttpClientDiagnosticsHelper | |
{ | |
private static readonly AsyncLocal<StrongBox<IPEndPoint>> s_endPoint = new(); | |
public static void ConfigureDebugConnectCallback(SocketsHttpHandler handler) | |
{ | |
var innerCallback = handler.ConnectCallback ?? DefaultConnectCallback; | |
handler.ConnectCallback = async (context, ct) => | |
{ | |
Stream s = await innerCallback(context, ct); | |
Debug.Assert(s is NetworkStream, "This helper only works on NetworkStreams"); | |
if (s is NetworkStream ns && ns.Socket.RemoteEndPoint is IPEndPoint endPoint) | |
{ | |
s = new DiagStream(ns, endPoint); | |
} | |
return s; | |
}; | |
static async ValueTask<Stream> DefaultConnectCallback(SocketsHttpConnectionContext context, CancellationToken ct) | |
{ | |
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; | |
try | |
{ | |
await socket.ConnectAsync(context.DnsEndPoint, ct); | |
return new NetworkStream(socket, ownsSocket: true); | |
} | |
catch | |
{ | |
socket.Dispose(); | |
throw; | |
} | |
} | |
} | |
public static async Task<(HttpResponseMessage Response, IPEndPoint EndPoint, Exception Ex)> SendAsync(HttpMessageInvoker client, HttpRequestMessage request, CancellationToken ct = default) | |
{ | |
if (request.Version.Major != 1 || request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher) | |
{ | |
throw new ArgumentException("This helper only works for HTTP/1.X requests.", nameof(request)); | |
} | |
var box = new StrongBox<IPEndPoint>(); | |
s_endPoint.Value = box; | |
try | |
{ | |
HttpResponseMessage response = await client.SendAsync(request, ct); | |
return (response, box.Value, null); | |
} | |
catch (Exception ex) | |
{ | |
return (null, box.Value, ex); | |
} | |
} | |
private sealed class DiagStream : Stream | |
{ | |
private readonly NetworkStream _innerStream; | |
private readonly IPEndPoint _endPoint; | |
public DiagStream(NetworkStream innerStream, IPEndPoint endPoint) | |
{ | |
_innerStream = innerStream; | |
_endPoint = endPoint; | |
} | |
private void OnWrite() | |
{ | |
if (s_endPoint.Value is StrongBox<IPEndPoint> box) | |
{ | |
box.Value = _endPoint; | |
} | |
} | |
public override void Write(byte[] buffer, int offset, int count) | |
{ | |
OnWrite(); | |
_innerStream.Write(buffer, offset, count); | |
} | |
public override void Write(ReadOnlySpan<byte> buffer) | |
{ | |
OnWrite(); | |
_innerStream.Write(buffer); | |
} | |
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |
{ | |
OnWrite(); | |
return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); | |
} | |
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) | |
{ | |
OnWrite(); | |
return _innerStream.WriteAsync(buffer, cancellationToken); | |
} | |
public override void WriteByte(byte value) | |
{ | |
OnWrite(); | |
_innerStream.WriteByte(value); | |
} | |
public override bool CanRead => _innerStream.CanRead; | |
public override bool CanSeek => _innerStream.CanSeek; | |
public override bool CanWrite => _innerStream.CanWrite; | |
public override long Length => _innerStream.Length; | |
public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } | |
public override void Flush() => _innerStream.Flush(); | |
public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken); | |
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); | |
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.ReadAsync(buffer, offset, count, cancellationToken); | |
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => _innerStream.ReadAsync(buffer, cancellationToken); | |
public override int ReadByte() => _innerStream.ReadByte(); | |
public override int Read(Span<byte> buffer) => _innerStream.Read(buffer); | |
public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); | |
public override void SetLength(long value) => _innerStream.SetLength(value); | |
public override ValueTask DisposeAsync() => _innerStream.DisposeAsync(); | |
protected override void Dispose(bool disposing) | |
{ | |
if (disposing) | |
{ | |
_innerStream.Dispose(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment