@@ -700,6 +700,11 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
700700 // inside the TLS client, and so we can't deinitialize the tls_client)
701701 redirect : ? Reader.Redirect = null ,
702702
703+ // There can be cases where we're forced to read the whole body into
704+ // memory in order to process it (*cough* CloudFront incorrectly sending
705+ // gzipped responses *cough*)
706+ full_body : ? std .ArrayListUnmanaged (u8 ) = null ,
707+
703708 const Self = @This ();
704709 const SendQueue = std .DoublyLinkedList ([]const u8 );
705710
@@ -911,8 +916,72 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
911916 return .done ;
912917 }
913918
919+ if (would_be_first ) {
920+ if (reader .response .get ("content-encoding" )) | ce | {
921+ if (std .ascii .eqlIgnoreCase (ce , "gzip" ) == false ) {
922+ self .handleError ("unsupported content encoding" , error .UnsupportedContentEncoding );
923+ return .done ;
924+ }
925+ // Our requests _do not_ include an Accept-Encoding header
926+ // but some servers (e.g. CloudFront) can send gzipped
927+ // responses nonetheless. Zig's compression libraries
928+ // do not work well with our async flow - they expect
929+ // to be able to read(buf) more data as needed, instead
930+ // of having us yield new data as it becomes available.
931+ // If async ever becomes a first class citizen, we could
932+ // expect this problem to go away. But, for now, we're
933+ // going to read the _whole_ body into memory. It makes
934+ // our life a lot easier, but it's still a mess.
935+ self .full_body = .empty ;
936+ }
937+ }
938+
914939 const done = result .done ;
915- if (result .data != null or done or would_be_first ) {
940+
941+ // see a few lines up, if this isn't null, something decided
942+ // we should buffer the entire body into memory.
943+ if (self .full_body ) | * full_body | {
944+ if (result .data ) | chunk | {
945+ full_body .appendSlice (self .request .arena , chunk ) catch | err | {
946+ self .handleError ("response buffering error" , err );
947+ return .done ;
948+ };
949+ }
950+
951+ // when buffering the body into memory, we only emit it once
952+ // everything is done (because we need to process the body
953+ // as a whole)
954+ if (done ) {
955+ // We should probably keep track of _why_ we're buffering
956+ // the body into memory. But, for now, the only possible
957+ // reason is that the response was gzipped. That means
958+ // we need to decompress it.
959+ var fbs = std .io .fixedBufferStream (full_body .items );
960+ var decompressor = std .compress .gzip .decompressor (fbs .reader ());
961+ var next = decompressor .next () catch | err | {
962+ self .handleError ("decompression error" , err );
963+ return .done ;
964+ };
965+
966+ var first = true ;
967+ while (next ) | chunk | {
968+ // we need to know if there's another chunk so that
969+ // we know if done should be true or false
970+ next = decompressor .next () catch | err | {
971+ self .handleError ("decompression error" , err );
972+ return .done ;
973+ };
974+ self .handler .onHttpResponse (.{
975+ .data = chunk ,
976+ .first = first ,
977+ .done = next == null ,
978+ .header = reader .response ,
979+ }) catch return .done ;
980+
981+ first = false ;
982+ }
983+ }
984+ } else if (result .data != null or done or would_be_first ) {
916985 // If we have data. Or if the request is done. Or if this is the
917986 // first time we have a complete header. Emit the chunk.
918987 self .handler .onHttpResponse (.{
@@ -1157,7 +1226,7 @@ const SyncHandler = struct {
11571226 // See CompressedReader for an explanation. This isn't great code. Sorry.
11581227 if (reader .response .get ("content-encoding" )) | ce | {
11591228 if (std .ascii .eqlIgnoreCase (ce , "gzip" ) == false ) {
1160- log .err ("unsupported content encoding '{s}' for: {}" , .{ ce , request .request_uri });
1229+ log .warn ("unsupported content encoding '{s}' for: {}" , .{ ce , request .request_uri });
11611230 return error .UnsupportedContentEncoding ;
11621231 }
11631232
@@ -2592,6 +2661,28 @@ test "HttpClient: async with body" {
25922661 });
25932662}
25942663
2664+ test "HttpClient: async with gzip body" {
2665+ var client = try testClient ();
2666+ defer client .deinit ();
2667+
2668+ var handler = try CaptureHandler .init ();
2669+ defer handler .deinit ();
2670+
2671+ const uri = try Uri .parse ("HTTP://127.0.0.1:9582/http_client/gzip" );
2672+ var req = try client .request (.GET , & uri );
2673+ try req .sendAsync (& handler .loop , & handler , .{});
2674+ try handler .waitUntilDone ();
2675+
2676+ const res = handler .response ;
2677+ try testing .expectEqual ("A new browser built for machines\n " , res .body .items );
2678+ try testing .expectEqual (200 , res .status );
2679+ try res .assertHeaders (&.{
2680+ "content-length" , "63" ,
2681+ "connection" , "close" ,
2682+ "content-encoding" , "gzip" ,
2683+ });
2684+ }
2685+
25952686test "HttpClient: async redirect" {
25962687 var client = try testClient ();
25972688 defer client .deinit ();
0 commit comments