1+ using System . Text ;
2+ using Microsoft . AspNetCore . Http ;
3+
4+ namespace SharedKernel . Logging . Middleware ;
5+
6+ internal static class HttpLogHelper
7+ {
8+ // defaults; can be overridden via Configure(options)
9+
10+ public static async Task < ( string Headers , string Body ) > CaptureAsync ( Stream bodyStream ,
11+ IHeaderDictionary headers ,
12+ string ? mediaType )
13+ {
14+ var hdrs = RedactionHelper . RedactHeaders ( headers ) ;
15+
16+ if ( ! IsTextLike ( mediaType ) )
17+ {
18+ long ? len = null ;
19+ if ( headers . TryGetValue ( "Content-Length" , out var clVal ) &&
20+ long . TryParse ( clVal . ToString ( ) , out var cl ) )
21+ len = cl ;
22+
23+ return ( LogFormatting . Json ( hdrs ) ,
24+ LogFormatting . Json ( BuildOmittedBodyMessage ( "non-text" ,
25+ len ,
26+ mediaType ,
27+ LoggingOptions . RequestResponseBodyMaxBytes ) ) ) ;
28+ }
29+
30+ var ( raw , truncated ) = await ReadLimitedAsync ( bodyStream , LoggingOptions . RequestResponseBodyMaxBytes ) ;
31+
32+ if ( truncated )
33+ return ( LogFormatting . Json ( hdrs ) ,
34+ LogFormatting . Json ( $ "[OMITTED: body exceeds { LoggingOptions . RequestResponseBodyMaxBytes / 1024 } KB]") ) ;
35+
36+ var body = RedactionHelper . RedactBody ( mediaType , raw ) ;
37+ return ( LogFormatting . Json ( hdrs ) , LogFormatting . Json ( body ) ) ;
38+ }
39+
40+ public static async Task < ( string Headers , string Body ) > CaptureAsync ( Dictionary < string , IEnumerable < string > > headers ,
41+ Func < Task < string > > rawReader ,
42+ string ? mediaType )
43+ {
44+ var hdrs = RedactionHelper . RedactHeaders ( headers ) ;
45+
46+ if ( ! IsTextLike ( mediaType ) )
47+ return ( LogFormatting . Json ( hdrs ) , LogFormatting . Json ( string . Empty ) ) ;
48+
49+ var raw = await rawReader ( ) ;
50+ if ( Utf8ByteCount ( raw ) > LoggingOptions . RequestResponseBodyMaxBytes )
51+ {
52+ return ( LogFormatting . Json ( hdrs ) ,
53+ LogFormatting . Json ( $ "[OMITTED: body exceeds { LoggingOptions . RequestResponseBodyMaxBytes / 1024 } KB]") ) ;
54+ }
55+
56+ var body = RedactionHelper . RedactBody ( mediaType , raw ) ;
57+ return ( LogFormatting . Json ( hdrs ) , LogFormatting . Json ( body ) ) ;
58+ }
59+
60+ public static Dictionary < string , IEnumerable < string > > CreateHeadersDictionary ( HttpRequestMessage req )
61+ {
62+ var dict = new Dictionary < string , IEnumerable < string > > ( StringComparer . OrdinalIgnoreCase ) ;
63+ foreach ( var h in req . Headers ) dict [ h . Key ] = h . Value ;
64+
65+ var contentHeaders = req . Content ? . Headers ;
66+ if ( contentHeaders != null )
67+ foreach ( var h in contentHeaders )
68+ dict [ h . Key ] = h . Value ;
69+
70+ return dict ;
71+ }
72+
73+ public static Dictionary < string , IEnumerable < string > > CreateHeadersDictionary ( HttpResponseMessage res )
74+ {
75+ var dict = new Dictionary < string , IEnumerable < string > > ( StringComparer . OrdinalIgnoreCase ) ;
76+ foreach ( var h in res . Headers ) dict [ h . Key ] = h . Value ;
77+ foreach ( var h in res . Content . Headers ) dict [ h . Key ] = h . Value ;
78+ return dict ;
79+ }
80+
81+ internal static string BuildOmittedBodyMessage ( string reason ,
82+ long ? lengthBytes ,
83+ string ? mediaType ,
84+ int thresholdBytes ) =>
85+ LogFormatting . Omitted ( reason , lengthBytes , mediaType , thresholdBytes ) ;
86+
87+ internal static bool IsTextLike ( string ? mediaType )
88+ {
89+ if ( string . IsNullOrWhiteSpace ( mediaType ) ) return false ;
90+ return LoggingOptions . TextLikeMediaPrefixes . Any ( m => mediaType . StartsWith ( m , StringComparison . OrdinalIgnoreCase ) )
91+ || mediaType . EndsWith ( "+json" , StringComparison . OrdinalIgnoreCase ) ;
92+ }
93+
94+ private static async Task < ( string text , bool truncated ) > ReadLimitedAsync ( Stream s , int maxBytes )
95+ {
96+ s . Seek ( 0 , SeekOrigin . Begin ) ;
97+
98+ using var ms = new MemoryStream ( capacity : maxBytes ) ;
99+ var buf = new byte [ Math . Min ( 8192 , maxBytes ) ] ;
100+ var total = 0 ;
101+
102+ while ( total < maxBytes )
103+ {
104+ var toRead = Math . Min ( buf . Length , maxBytes - total ) ;
105+ var read = await s . ReadAsync ( buf . AsMemory ( 0 , toRead ) ) ;
106+ if ( read == 0 ) break ;
107+ await ms . WriteAsync ( buf . AsMemory ( 0 , read ) ) ;
108+ total += read ;
109+ }
110+
111+ var truncated = false ;
112+ if ( total == maxBytes )
113+ {
114+ var probe = new byte [ 1 ] ;
115+ var read = await s . ReadAsync ( probe . AsMemory ( 0 , 1 ) ) ;
116+ if ( read > 0 )
117+ {
118+ truncated = true ;
119+ if ( s . CanSeek ) s . Seek ( - read , SeekOrigin . Current ) ;
120+ }
121+ }
122+
123+ s . Seek ( 0 , SeekOrigin . Begin ) ;
124+ return ( Encoding . UTF8 . GetString ( ms . ToArray ( ) ) , truncated ) ;
125+ }
126+
127+ private static int Utf8ByteCount ( string s ) => Encoding . UTF8 . GetByteCount ( s ) ;
128+ }
0 commit comments