Skip to content

Commit 7156df8

Browse files
committed
Add support for gzip responses in AsyncHandler
Compliments #601 which added this behavior to the SyncHandler.
1 parent 210d4f6 commit 7156df8

File tree

1 file changed

+93
-2
lines changed

1 file changed

+93
-2
lines changed

src/http/client.zig

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
25952686
test "HttpClient: async redirect" {
25962687
var client = try testClient();
25972688
defer client.deinit();

0 commit comments

Comments
 (0)