11using System ;
22using System . IO ;
3+ using System . IO . Compression ;
34using System . Net ;
45using System . Net . Http ;
56using System . Text . Json ;
@@ -21,7 +22,7 @@ internal static class ErrorHandler
2122 {
2223 PropertyNameCaseInsensitive = true ,
2324 } ;
24-
25+
2526 public static async Task < DdbException > ProcessErrorAsync ( DynamoDbContextMetadata metadata , HttpResponseMessage response , CancellationToken cancellationToken = default )
2627 {
2728 try
@@ -30,37 +31,71 @@ public static async Task<DdbException> ProcessErrorAsync(DynamoDbContextMetadata
3031 if ( response . StatusCode == HttpStatusCode . ServiceUnavailable )
3132 return new ServiceUnavailableException ( "DynamoDB is currently unavailable. (This should be a temporary state.)" ) ;
3233
33- var recyclableStream = new RecyclableMemoryStream ( DynamoDbHttpContent . MemoryStreamManager ) ;
34- try
35- {
36- await responseStream . CopyToAsync ( recyclableStream , cancellationToken ) . ConfigureAwait ( false ) ;
37-
38- recyclableStream . Position = 0 ;
39- var error = await JsonSerializer . DeserializeAsync < Error > ( recyclableStream , SerializerOptions , cancellationToken ) . ConfigureAwait ( false ) ;
40- recyclableStream . Position = 0 ;
41-
42- switch ( response . StatusCode )
43- {
44- case HttpStatusCode . BadRequest :
45- return await ProcessBadRequestAsync ( metadata , recyclableStream , error , cancellationToken ) . ConfigureAwait ( false ) ;
46- case HttpStatusCode . InternalServerError :
47- return new InternalServerErrorException ( error . Message ) ;
48- default :
49- return new DdbException ( error . Message ) ;
50- }
51- }
52- finally
34+ var hasGzipHeader = response . Content . Headers . ContentEncoding . Contains ( "gzip" ) ;
35+ await using var parsedStreamOwner = await DecodeErrorStreamAsync ( responseStream , hasGzipHeader , cancellationToken ) . ConfigureAwait ( false ) ;
36+
37+ var errorStream = parsedStreamOwner . Stream ;
38+ var error = await JsonSerializer . DeserializeAsync < Error > ( errorStream , SerializerOptions , cancellationToken ) . ConfigureAwait ( false ) ;
39+ errorStream . Position = 0 ;
40+
41+ return response . StatusCode switch
5342 {
54- await recyclableStream . DisposeAsync ( ) . ConfigureAwait ( false ) ;
55- }
43+ HttpStatusCode . BadRequest => await ProcessBadRequestAsync ( metadata , errorStream , error , cancellationToken ) . ConfigureAwait ( false ) ,
44+ HttpStatusCode . InternalServerError => new InternalServerErrorException ( error . Message ) ,
45+ _ => new DdbException ( error . Message )
46+ } ;
5647 }
5748 finally
5849 {
5950 response . Dispose ( ) ;
6051 }
6152 }
53+
54+ private static bool IsGzipEncoded ( RecyclableMemoryStream stream )
55+ {
56+ Span < byte > magic = stackalloc byte [ 2 ] ;
57+ _ = stream . Read ( magic ) ;
58+ stream . Position = 0 ;
59+ return magic [ 0 ] == 0x1F && magic [ 1 ] == 0x8B ;
60+ }
61+
62+ private static async Task < DecodedStreamOwner > DecodeErrorStreamAsync ( Stream responseStream , bool hasGzipHeader , CancellationToken ct = default )
63+ {
64+ var recyclableStream = new RecyclableMemoryStream ( DynamoDbHttpContent . MemoryStreamManager ) ;
65+ RecyclableMemoryStream ? decompressedStream = null ;
66+ try
67+ {
68+ await responseStream . CopyToAsync ( recyclableStream , ct ) . ConfigureAwait ( false ) ;
69+ recyclableStream . Position = 0 ;
70+ if ( ! hasGzipHeader || recyclableStream . Length < 2 )
71+ return new ( recyclableStream , null ) ;
72+
73+ // DynamoDB lies: error responses may carry Content-Encoding: gzip but the body is NOT gzipped.
74+ // When the header claims gzip, detect actual gzip by inspecting magic bytes (0x1F 0x8B).
75+ if ( ! IsGzipEncoded ( recyclableStream ) )
76+ return new ( recyclableStream , null ) ;
77+
78+ // Rare path: error response was actually gzip-encoded — decompress eagerly into a pooled stream.
79+ decompressedStream = new RecyclableMemoryStream ( DynamoDbHttpContent . MemoryStreamManager ) ;
80+ await using ( var gz = new GZipStream ( recyclableStream , CompressionMode . Decompress , leaveOpen : true ) )
81+ {
82+ await gz . CopyToAsync ( decompressedStream , ct ) . ConfigureAwait ( false ) ;
83+ }
84+
85+ recyclableStream . Position = 0 ;
86+ decompressedStream . Position = 0 ;
87+ return new ( recyclableStream , decompressedStream ) ;
88+ }
89+ catch ( Exception )
90+ {
91+ await recyclableStream . DisposeAsync ( ) . ConfigureAwait ( false ) ;
92+ if ( decompressedStream is not null )
93+ await decompressedStream . DisposeAsync ( ) . ConfigureAwait ( false ) ;
94+ throw ;
95+ }
96+ }
6297
63- private static ValueTask < DdbException > ProcessBadRequestAsync ( DynamoDbContextMetadata metadata , MemoryStream recyclableStream , Error error , CancellationToken cancellationToken )
98+ private static ValueTask < DdbException > ProcessBadRequestAsync ( DynamoDbContextMetadata metadata , Stream recyclableStream , Error error , CancellationToken cancellationToken )
6499 {
65100 if ( error . Type is null )
66101 return new ( new DdbException ( string . Empty ) ) ;
@@ -122,6 +157,33 @@ async ValueTask<DdbException> ParseConditionalCheckFailedException()
122157 return new ConditionalCheckFailedException ( conditionalCheckFailedResponse . Value ! . Item , error . Message ) ;
123158 }
124159 }
160+
161+ private readonly struct DecodedStreamOwner : IAsyncDisposable , IDisposable
162+ {
163+ private readonly RecyclableMemoryStream _original ;
164+ private readonly RecyclableMemoryStream ? _decompressed ;
165+
166+ public DecodedStreamOwner ( RecyclableMemoryStream original , RecyclableMemoryStream ? decompressed )
167+ {
168+ _original = original ;
169+ _decompressed = decompressed ;
170+ }
171+
172+ public Stream Stream => _decompressed ?? _original ;
173+
174+ public async ValueTask DisposeAsync ( )
175+ {
176+ await _original . DisposeAsync ( ) . ConfigureAwait ( false ) ;
177+ if ( _decompressed is not null )
178+ await _decompressed . DisposeAsync ( ) . ConfigureAwait ( false ) ;
179+ }
180+
181+ public void Dispose ( )
182+ {
183+ _original . Dispose ( ) ;
184+ _decompressed ? . Dispose ( ) ;
185+ }
186+ }
125187
126188 private readonly struct Error
127189 {
0 commit comments