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+ }
0 commit comments