Skip to content

Commit 3c655c9

Browse files
author
LoneWandererProductions
committed
add a HTTP Tracer
1 parent d250f2c commit 3c655c9

2 files changed

Lines changed: 253 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: Communication
4+
* FILE: HttpWireTracingHandler.cs
5+
* PURPOSE: HTTP Message Handler middleware for Insomnia-like wire tracing with Trace/String toggle
6+
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
*/
8+
9+
using System;
10+
using System.Diagnostics;
11+
using System.Net.Http;
12+
using System.Text;
13+
using System.Threading;
14+
using System.Threading.Tasks;
15+
16+
namespace Communication
17+
{
18+
/// <summary>
19+
/// Intercept Http messages and trace results. Supports toggling between System.Diagnostics.Trace and String output.
20+
/// </summary>
21+
/// <seealso cref="DelegatingHandler" />
22+
public class HttpWireTracingHandler : DelegatingHandler
23+
{
24+
private readonly object _lockObj = new object();
25+
private string _lastTrace = string.Empty;
26+
27+
/// <summary>
28+
/// Gets or sets a value indicating whether the log should be written to <see cref="System.Diagnostics.Trace"/>.
29+
/// Default is true.
30+
/// </summary>
31+
public bool LogToTrace { get; set; } = true;
32+
33+
/// <summary>
34+
/// Optional callback that triggers whenever a new full wire trace string is generated.
35+
/// Perfect for UI bindings (e.g., updating a Blazor log terminal).
36+
/// </summary>
37+
public Action<string>? OnTraceCaptured { get; set; }
38+
39+
/// <summary>
40+
/// Holds the exact string content of the very last captured HTTP wire trace.
41+
/// </summary>
42+
public string LastTrace
43+
{
44+
get
45+
{
46+
lock (_lockObj) { return _lastTrace; }
47+
}
48+
private set
49+
{
50+
lock (_lockObj) { _lastTrace = value; }
51+
}
52+
}
53+
54+
/// <summary>
55+
/// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.
56+
/// </summary>
57+
/// <param name="request">The HTTP request message to send to the server.</param>
58+
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
59+
/// <returns>
60+
/// The task object representing the asynchronous operation.
61+
/// </returns>
62+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
63+
{
64+
// Lokaler Speicher für diese spezifische Anfrage (Thread-Safety!)
65+
var sb = new StringBuilder();
66+
67+
sb.AppendLine("\n* ------------------ WIRE TRACE START ------------------ *");
68+
69+
// 1. REQUEST LOGGING (>)
70+
sb.AppendLine($"\n> {request.Method} {request.RequestUri.PathAndQuery} HTTP/{request.Version}");
71+
sb.AppendLine($"> Host: {request.RequestUri.Host}");
72+
73+
foreach (var header in request.Headers)
74+
{
75+
sb.AppendLine($"> {header.Key}: {string.Join(", ", header.Value)}");
76+
}
77+
78+
if (request.Content != null)
79+
{
80+
foreach (var header in request.Content.Headers)
81+
{
82+
sb.AppendLine($"> {header.Key}: {string.Join(", ", header.Value)}");
83+
}
84+
85+
// Buffer content safely so we don't disrupt the stream
86+
await request.Content.LoadIntoBufferAsync();
87+
var reqBody = await request.Content.ReadAsStringAsync();
88+
if (!string.IsNullOrEmpty(reqBody))
89+
{
90+
sb.AppendLine("> ");
91+
sb.AppendLine(reqBody.Length > 1000 ? reqBody.Substring(0, 1000) + "... [TRUNCATED]" : reqBody);
92+
}
93+
}
94+
95+
sb.AppendLine("\n* Technical upload complete, awaiting response...");
96+
97+
// Execute the actual HTTP Call
98+
var response = await base.SendAsync(request, cancellationToken);
99+
100+
// 2. RESPONSE LOGGING (<)
101+
sb.AppendLine($"\n< HTTP/{response.Version} {(int)response.StatusCode} {response.StatusCode}");
102+
103+
foreach (var header in response.Headers)
104+
{
105+
sb.AppendLine($"< {header.Key}: {string.Join(", ", header.Value)}");
106+
}
107+
if (response.Content != null)
108+
{
109+
foreach (var header in response.Content.Headers)
110+
{
111+
sb.AppendLine($"< {header.Key}: {string.Join(", ", header.Value)}");
112+
}
113+
114+
var resBody = await response.Content.ReadAsStringAsync();
115+
if (!string.IsNullOrEmpty(resBody))
116+
{
117+
sb.AppendLine("< ");
118+
sb.AppendLine(resBody);
119+
}
120+
}
121+
122+
sb.AppendLine("\n* ------------------- WIRE TRACE END ------------------- *\n");
123+
124+
// --- AUSWERTUNG DES WECHSELSCHALTERS ---
125+
string finalTraceLog = sb.ToString();
126+
LastTrace = finalTraceLog;
127+
128+
// Schalter 1: Standard System.Diagnostics.Trace
129+
if (LogToTrace)
130+
{
131+
Trace.Write(finalTraceLog);
132+
}
133+
134+
// Schalter 2: Event/Callback für String-Verarbeitung abfeuern
135+
OnTraceCaptured?.Invoke(finalTraceLog);
136+
137+
return response;
138+
}
139+
}
140+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: CommunicationTests
4+
* FILE: HttpWireTracingHandlerTests.cs
5+
* PURPOSE: Unit tests for HttpWireTracingHandler ensuring correct request/response interception and tracing
6+
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
*/
8+
9+
using System.Diagnostics;
10+
using System.Net;
11+
using System.Text;
12+
using Communication;
13+
14+
namespace CommunicationTests
15+
{
16+
/// <summary>
17+
/// Test our new Http wire tracer.
18+
/// </summary>
19+
[TestClass]
20+
public class HttpWireTracingHandlerTests
21+
{
22+
/// <summary>
23+
/// The test URL
24+
/// </summary>
25+
private const string TestUrl = "https://localhost/test/api/endpoint";
26+
27+
/// <summary>
28+
/// Sends the asynchronous should log request and response to trace and return correct response.
29+
/// </summary>
30+
[TestMethod]
31+
public async Task SendAsync_ShouldLogRequestAndResponseToTrace_AndReturnCorrectResponse()
32+
{
33+
// Arrange
34+
using var stringWriter = new StringWriter();
35+
using var traceListener = new TextWriterTraceListener(stringWriter);
36+
Trace.Listeners.Add(traceListener);
37+
38+
var expectedResponseBody = "{\"status\":\"success\",\"message\":\"f81dcbd2\"}";
39+
var innerHandler = new MockHttpMessageHandler(HttpStatusCode.OK, expectedResponseBody);
40+
41+
var tracingHandler = new HttpWireTracingHandler
42+
{
43+
//switch Trace one and off
44+
LogToTrace = true,
45+
InnerHandler = innerHandler
46+
};
47+
48+
using var client = new HttpClient(tracingHandler);
49+
var requestContent = new StringContent("{\"input\":\"data_from_riesa\"}", Encoding.UTF8, "application/json");
50+
51+
try
52+
{
53+
// Act
54+
var response = await client.PostAsync(TestUrl, requestContent);
55+
var actualResponseBody = await response.Content.ReadAsStringAsync();
56+
57+
// Erzwinge das Schreiben aller gepufferten Trace-Daten
58+
Trace.Flush();
59+
var traceOutput = stringWriter.ToString();
60+
61+
// Assert 1: Überprüfung der echten HTTP-Funktionalität
62+
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
63+
Assert.AreEqual(expectedResponseBody, actualResponseBody);
64+
65+
// Assert 2: Überprüfung der Wire-Trace Struktur (Insomnia Layout)
66+
Assert.IsTrue(traceOutput.Contains("WIRE TRACE START"), "Trace missing start boundary.");
67+
Assert.IsTrue(traceOutput.Contains("WIRE TRACE END"), "Trace missing end boundary.");
68+
69+
// Assert 3: Validierung der Request-Daten (>)
70+
Assert.IsTrue(traceOutput.Contains("> POST /test/api/endpoint"), "Request method or path path not traced correctly.");
71+
Assert.IsTrue(traceOutput.Contains("> Host: localhost"), "Request host header missing in trace.");
72+
Assert.IsTrue(traceOutput.Contains("Content-Type: application/json"), "Content-Type header missing in request trace.");
73+
Assert.IsTrue(traceOutput.Contains("{\"input\":\"data_from_riesa\"}"), "Request body payload missing in trace.");
74+
75+
// Assert 4: Validierung der Response-Daten (<)
76+
Assert.IsTrue(traceOutput.Contains("< HTTP/1.1 200 OK"), "Response status code line missing or incorrect in trace.");
77+
Assert.IsTrue(traceOutput.Contains(expectedResponseBody), "Response body payload missing in trace.");
78+
}
79+
finally
80+
{
81+
// Berebereinigung des Trace-Listeners, um Seiteneffekte auf andere Tests zu verhindern
82+
Trace.Listeners.Remove(traceListener);
83+
}
84+
}
85+
86+
/// <summary>
87+
/// A local HTTP mock handler for simulating network traffic without using the physical connection.
88+
/// </summary>
89+
/// <seealso cref="HttpMessageHandler" />
90+
private class MockHttpMessageHandler : HttpMessageHandler
91+
{
92+
private readonly HttpStatusCode _statusCode;
93+
private readonly string _content;
94+
95+
public MockHttpMessageHandler(HttpStatusCode statusCode, string content)
96+
{
97+
_statusCode = statusCode;
98+
_content = content;
99+
}
100+
101+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
102+
{
103+
var response = new HttpResponseMessage(_statusCode)
104+
{
105+
Content = new StringContent(_content, Encoding.UTF8, "application/json"),
106+
Version = request.Version
107+
};
108+
109+
return Task.FromResult(response);
110+
}
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)