@@ -125,7 +125,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
125125 effectiveInput ? . Dispose ( ) ;
126126 }
127127
128- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions ) . ConfigureAwait ( false ) ;
128+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , cancellationToken ) . ConfigureAwait ( false ) ;
129129 }
130130
131131 /// <inheritdoc/>
@@ -151,7 +151,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
151151 effectiveInput ? . Dispose ( ) ;
152152 }
153153
154- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions ) . ConfigureAwait ( false ) ;
154+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , cancellationToken ) . ConfigureAwait ( false ) ;
155155 }
156156
157157 /// <inheritdoc/>
@@ -168,7 +168,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
168168 HttpResponseMessage response = await CallApiInternalAsync ( serviceName , effectiveOptions , true ,
169169 null , null , cancellationToken ) . ConfigureAwait ( false ) ;
170170
171- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions ) . ConfigureAwait ( false ) ;
171+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , cancellationToken ) . ConfigureAwait ( false ) ;
172172 }
173173
174174 /// <inheritdoc/>
@@ -185,7 +185,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
185185 DownstreamApiOptions effectiveOptions = MergeOptions ( serviceName , downstreamApiOptionsOverride ) ;
186186 HttpResponseMessage response = await CallApiInternalAsync ( serviceName , effectiveOptions , false ,
187187 null , user , cancellationToken ) . ConfigureAwait ( false ) ;
188- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions ) . ConfigureAwait ( false ) ;
188+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , cancellationToken ) . ConfigureAwait ( false ) ;
189189 }
190190
191191#if NET8_0_OR_GREATER
@@ -212,7 +212,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
212212 effectiveInput ? . Dispose ( ) ;
213213 }
214214
215- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo ) . ConfigureAwait ( false ) ;
215+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo , cancellationToken ) . ConfigureAwait ( false ) ;
216216 }
217217
218218 /// <inheritdoc/>
@@ -227,7 +227,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
227227 DownstreamApiOptions effectiveOptions = MergeOptions ( serviceName , downstreamApiOptionsOverride ) ;
228228 HttpResponseMessage response = await CallApiInternalAsync ( serviceName , effectiveOptions , false ,
229229 null , user , cancellationToken ) . ConfigureAwait ( false ) ;
230- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo ) . ConfigureAwait ( false ) ;
230+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo , cancellationToken ) . ConfigureAwait ( false ) ;
231231 }
232232
233233 /// <inheritdoc/>
@@ -251,7 +251,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
251251 effectiveInput ? . Dispose ( ) ;
252252 }
253253
254- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo ) . ConfigureAwait ( false ) ;
254+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo , cancellationToken ) . ConfigureAwait ( false ) ;
255255 }
256256
257257 /// <inheritdoc/>
@@ -266,7 +266,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
266266 HttpResponseMessage response = await CallApiInternalAsync ( serviceName , effectiveOptions , true ,
267267 null , null , cancellationToken ) . ConfigureAwait ( false ) ;
268268
269- return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo ) . ConfigureAwait ( false ) ;
269+ return await DeserializeOutputAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo , cancellationToken ) . ConfigureAwait ( false ) ;
270270 }
271271
272272 internal static HttpContent ? SerializeInput < TInput > ( TInput input , DownstreamApiOptions effectiveOptions , JsonTypeInfo < TInput > inputJsonTypeInfo )
@@ -299,10 +299,10 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
299299 return httpContent ;
300300 }
301301
302- internal static async Task < TOutput ? > DeserializeOutputAsync < TOutput > ( HttpResponseMessage response , DownstreamApiOptions effectiveOptions , JsonTypeInfo < TOutput > outputJsonTypeInfo )
302+ internal static async Task < TOutput ? > DeserializeOutputAsync < TOutput > ( HttpResponseMessage response , DownstreamApiOptions effectiveOptions , JsonTypeInfo < TOutput > outputJsonTypeInfo , CancellationToken cancellationToken = default )
303303 where TOutput : class
304304 {
305- return await DeserializeOutputImplAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo ) ;
305+ return await DeserializeOutputImplAsync < TOutput > ( response , effectiveOptions , outputJsonTypeInfo , cancellationToken ) ;
306306 }
307307#endif
308308
@@ -396,7 +396,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
396396 [ RequiresUnreferencedCode ( "Calls JsonSerializer.Serialize<TInput>" ) ]
397397 [ RequiresDynamicCode ( "Calls JsonSerializer.Serialize<TInput>" ) ]
398398#endif
399- internal static async Task < TOutput ? > DeserializeOutputAsync < TOutput > ( HttpResponseMessage response , DownstreamApiOptions effectiveOptions )
399+ internal static async Task < TOutput ? > DeserializeOutputAsync < TOutput > ( HttpResponseMessage response , DownstreamApiOptions effectiveOptions , CancellationToken cancellationToken = default )
400400 where TOutput : class
401401 {
402402 try
@@ -405,7 +405,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
405405 }
406406 catch
407407 {
408- string error = await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ;
408+ string error = await ReadErrorResponseContentAsync ( response , cancellationToken ) . ConfigureAwait ( false ) ;
409409
410410#if NET5_0_OR_GREATER
411411 throw new HttpRequestException ( $ "{ ( int ) response . StatusCode } { response . StatusCode } { error } ", null , response . StatusCode ) ;
@@ -447,7 +447,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
447447 }
448448 }
449449
450- private static async Task < TOutput ? > DeserializeOutputImplAsync < TOutput > ( HttpResponseMessage response , DownstreamApiOptions effectiveOptions , JsonTypeInfo < TOutput > outputJsonTypeInfo )
450+ private static async Task < TOutput ? > DeserializeOutputImplAsync < TOutput > ( HttpResponseMessage response , DownstreamApiOptions effectiveOptions , JsonTypeInfo < TOutput > outputJsonTypeInfo , CancellationToken cancellationToken = default )
451451 where TOutput : class
452452 {
453453 try
@@ -456,7 +456,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
456456 }
457457 catch
458458 {
459- string error = await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ;
459+ string error = await ReadErrorResponseContentAsync ( response , cancellationToken ) . ConfigureAwait ( false ) ;
460460
461461#if NET5_0_OR_GREATER
462462 throw new HttpRequestException ( $ "{ ( int ) response . StatusCode } { response . StatusCode } { error } ", null , response . StatusCode ) ;
@@ -645,7 +645,46 @@ private static void AddCallerSDKTelemetry(DownstreamApiOptions effectiveOptions)
645645 CallerSDKDetails [ "caller-sdk-id" ] ;
646646 effectiveOptions . AcquireTokenOptions . ExtraQueryParameters [ "caller-sdk-ver" ] =
647647 CallerSDKDetails [ "caller-sdk-ver" ] ;
648- }
649- }
650- }
648+ }
649+ }
650+
651+ /// <summary>
652+ /// Safely reads error response content with size limits to avoid performance issues with large payloads.
653+ /// </summary>
654+ /// <param name="response">The HTTP response message.</param>
655+ /// <param name="cancellationToken">Cancellation token.</param>
656+ /// <returns>The error response content, truncated if necessary.</returns>
657+ internal static async Task < string > ReadErrorResponseContentAsync ( HttpResponseMessage response , CancellationToken cancellationToken = default )
658+ {
659+ const int maxErrorContentLength = 4096 ;
660+
661+ long ? contentLength = response . Content . Headers . ContentLength ;
662+
663+ if ( contentLength . HasValue && contentLength . Value > maxErrorContentLength )
664+ {
665+ return $ "[Error response too large: { contentLength . Value } bytes, not captured]";
666+ }
667+
668+ // Use streaming to read only up to maxErrorContentLength to avoid loading entire response into memory
669+ #if NET5_0_OR_GREATER
670+ using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
671+ #else
672+ using var stream = await response . Content . ReadAsStreamAsync ( ) . ConfigureAwait ( false ) ;
673+ #endif
674+ using var reader = new StreamReader ( stream ) ;
675+
676+ char [ ] buffer = new char [ maxErrorContentLength ] ;
677+ int readCount = await reader . ReadBlockAsync ( buffer , 0 , maxErrorContentLength ) . ConfigureAwait ( false ) ;
678+
679+ string errorResponseContent = new string ( buffer , 0 , readCount ) ;
680+
681+ // Check if there's more content that was truncated
682+ if ( readCount == maxErrorContentLength && reader . Peek ( ) != - 1 )
683+ {
684+ errorResponseContent += "... (truncated)" ;
685+ }
686+
687+ return errorResponseContent ;
688+ }
689+ }
651690}
0 commit comments