11using System . Diagnostics ;
2+ using System . Net ;
3+ using System . Text ;
24using DebugProbe . AspNetCore . Internal . Utils ;
35using DebugProbe . AspNetCore . Models ;
46using DebugProbe . AspNetCore . Options ;
@@ -33,13 +35,13 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
3335 {
3436 var response = await base . SendAsync ( request , cancellationToken ) ;
3537
36- await CaptureRequest ( request , response , null , started . ElapsedMilliseconds ) ;
38+ await CaptureRequest ( request , response , null , started . ElapsedMilliseconds , cancellationToken ) ;
3739
3840 return response ;
3941 }
4042 catch ( Exception ex )
4143 {
42- await CaptureRequest ( request , null , ex , started . ElapsedMilliseconds ) ;
44+ await CaptureRequest ( request , null , ex , started . ElapsedMilliseconds , cancellationToken ) ;
4345
4446 throw ;
4547 }
@@ -48,7 +50,12 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
4850 /// <summary>
4951 /// Captures outgoing request details and stores them in the active DebugProbe entry.
5052 /// </summary>
51- private async Task CaptureRequest ( HttpRequestMessage request , HttpResponseMessage ? response , Exception ? exception , long durationMs )
53+ private async Task CaptureRequest (
54+ HttpRequestMessage request ,
55+ HttpResponseMessage ? response ,
56+ Exception ? exception ,
57+ long durationMs ,
58+ CancellationToken cancellationToken )
5259 {
5360 if ( ! TryGetActiveEntry ( out var entry ) )
5461 {
@@ -76,9 +83,9 @@ private async Task CaptureRequest(HttpRequestMessage request, HttpResponseMessag
7683 ResponseHeaders = response != null ? response . Headers . ToDictionary ( x => x . Key , x => RedactionUtils . RedactHeader ( x . Key , string . Join ( ", " , x . Value ) , _options ) ) : [ ]
7784 } ;
7885
79- outgoing . RequestBody = await CaptureBodyAsync ( request . Content ) ;
86+ outgoing . RequestBody = ( await CaptureBodyAsync ( request . Content , cancellationToken ) ) . Body ;
8087
81- outgoing . ResponseBody = await CaptureBodyAsync ( response ? . Content ) ;
88+ outgoing . ResponseBody = await CaptureResponseBodyAsync ( response , cancellationToken ) ;
8289
8390 entry . OutgoingRequests . Add ( outgoing ) ;
8491 }
@@ -100,7 +107,25 @@ private bool TryGetActiveEntry(out DebugEntry entry)
100107 return true ;
101108 }
102109
103- private async Task < string > CaptureBodyAsync ( HttpContent ? content )
110+ private async Task < string > CaptureResponseBodyAsync ( HttpResponseMessage ? response , CancellationToken cancellationToken )
111+ {
112+ var content = response ? . Content ;
113+ if ( content == null )
114+ {
115+ return string . Empty ;
116+ }
117+
118+ var result = await CaptureBodyAsync ( content , cancellationToken ) ;
119+
120+ if ( result . BytesRead . Length > 0 )
121+ {
122+ response ! . Content = new PrefixReplayHttpContent ( content , result . Stream , result . BytesRead ) ;
123+ }
124+
125+ return result . Body ;
126+ }
127+
128+ private async Task < BodyCaptureResult > CaptureBodyAsync ( HttpContent ? content , CancellationToken cancellationToken )
104129 {
105130 if ( content == null ||
106131 ! HttpContentUtils . IsTextContent ( content . Headers . ContentType ? . MediaType ) )
@@ -115,24 +140,100 @@ private async Task<string> CaptureBodyAsync(HttpContent? content)
115140 var buffer = new byte [ limit + 1 ] ;
116141 var totalRead = 0 ;
117142
118- var stream = await content . ReadAsStreamAsync ( ) ;
143+ var stream = await content . ReadAsStreamAsync ( cancellationToken ) ;
119144
120145 int bytesRead ;
121146 while ( totalRead < buffer . Length &&
122- ( bytesRead = await stream . ReadAsync ( buffer . AsMemory ( totalRead , buffer . Length - totalRead ) ) ) > 0 )
147+ ( bytesRead = await stream . ReadAsync ( buffer . AsMemory ( totalRead , buffer . Length - totalRead ) , cancellationToken ) ) > 0 )
123148 {
124149 totalRead += bytesRead ;
125150 }
126151
127152 var truncated = totalRead > limit ;
128- var encoding = System . Text . Encoding . UTF8 ;
153+ var encoding = GetEncoding ( content ) ;
129154 var body = encoding . GetString ( buffer , 0 , Math . Min ( totalRead , limit ) ) ;
130155
131156 if ( truncated )
132157 {
133158 body += "[truncated]" ;
134159 }
135160
136- return JsonUtils . Format ( RedactionUtils . RedactJsonFields ( body , _options ) ) ;
161+ return new BodyCaptureResult (
162+ JsonUtils . Format ( RedactionUtils . RedactJsonFields ( body , _options ) ) ,
163+ stream ,
164+ buffer [ ..totalRead ] ) ;
165+ }
166+
167+ private static Encoding GetEncoding ( HttpContent content )
168+ {
169+ var charset = content . Headers . ContentType ? . CharSet ;
170+ if ( string . IsNullOrWhiteSpace ( charset ) )
171+ {
172+ return Encoding . UTF8 ;
173+ }
174+
175+ try
176+ {
177+ return Encoding . GetEncoding ( charset . Trim ( '"' ) ) ;
178+ }
179+ catch
180+ {
181+ return Encoding . UTF8 ;
182+ }
183+ }
184+
185+ private readonly record struct BodyCaptureResult ( string Body , Stream Stream , byte [ ] BytesRead )
186+ {
187+ public static implicit operator BodyCaptureResult ( string body ) => new ( body , Stream . Null , [ ] ) ;
188+ }
189+
190+ private sealed class PrefixReplayHttpContent : HttpContent
191+ {
192+ private readonly HttpContent _innerContent ;
193+
194+ private readonly Stream _remainingStream ;
195+
196+ private readonly byte [ ] _prefix ;
197+
198+ public PrefixReplayHttpContent ( HttpContent innerContent , Stream remainingStream , byte [ ] prefix )
199+ {
200+ _innerContent = innerContent ;
201+ _remainingStream = remainingStream ;
202+ _prefix = prefix ;
203+
204+ foreach ( var header in innerContent . Headers )
205+ {
206+ Headers . TryAddWithoutValidation ( header . Key , header . Value ) ;
207+ }
208+ }
209+
210+ protected override async Task SerializeToStreamAsync ( Stream stream , TransportContext ? context )
211+ {
212+ await stream . WriteAsync ( _prefix ) ;
213+ await _remainingStream . CopyToAsync ( stream ) ;
214+ }
215+
216+ protected override bool TryComputeLength ( out long length )
217+ {
218+ if ( _innerContent . Headers . ContentLength is { } contentLength )
219+ {
220+ length = contentLength ;
221+ return true ;
222+ }
223+
224+ length = 0 ;
225+ return false ;
226+ }
227+
228+ protected override void Dispose ( bool disposing )
229+ {
230+ if ( disposing )
231+ {
232+ _remainingStream . Dispose ( ) ;
233+ _innerContent . Dispose ( ) ;
234+ }
235+
236+ base . Dispose ( disposing ) ;
237+ }
137238 }
138239}
0 commit comments