gzip yer uploads
Compression is a two-way street
NSURLSession
opaquely handles HTTP negotiations about accepting compressed content and does the decompression (using formats like gzip and brotli) before you receive responses, which is great. Foundation
, Apple’s OSes, and the web are doing work that we as developers don’t have to so our users consume less data and have a faster, less data-consuming experience. Hooray! I’ve always been impressed that this happens pretty much auto-magically without having to think about it.
However, I had a thought late in 2024 about transmitting data the other way: uploads. While looking I realized there’s not automatic upload compression built into NSURLSession
. I’m sure there are good reasons for this, but I figured that some of the APIs I was using could accept compressed uploads. It turns out I was right, Mixpanel and Dropbox both accept compressed data.
Here’s how I’m doing the compression itself
#import <zlib.h>
// Function that gzips data
// Thanks Claude https://tijo.link/BGdVNx
static NSData *_gzipCompressData(NSData *const data, NSError **error) {
if (!data.length) {
if (error) {
*error = [NSError errorWithDomain:@"CompressionErrorDomain" code:1
userInfo:@{NSLocalizedDescriptionKey: @"Invalid input data"}];
}
return nil;
}
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// Initialize deflate with gzip format
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
if (error) {
*error = [NSError errorWithDomain:@"CompressionErrorDomain" code:2
userInfo:@{NSLocalizedDescriptionKey: @"Failed to initialize compression"}];
}
return nil;
}
// Set up input
strm.avail_in = (uInt)data.length;
strm.next_in = (Bytef *)data.bytes;
// Prepare output buffer (compress can increase size)
NSMutableData *compressedData = [NSMutableData dataWithLength:data.length * 1.1 + 12];
strm.avail_out = (uInt)compressedData.length;
strm.next_out = compressedData.mutableBytes;
// Compress
if (deflate(&strm, Z_FINISH) != Z_STREAM_END) {
deflateEnd(&strm);
if (error) {
*error = [NSError errorWithDomain:@"CompressionErrorDomain" code:3
userInfo:@{NSLocalizedDescriptionKey: @"Compression failed"}];
}
return nil;
}
// Cleanup and finalize
[compressedData setLength:strm.total_out];
deflateEnd(&strm);
return compressedData;
}
(You could also use a library like this one if you don’t want to hand write the compression.)
Then, if using a data task:
NSData *compressedData = _gzipCompressData(originalData, &err);
NSURLRequest *request = ...;
[request setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
[request setHTTPBody:compressedData];
// Start a data task using request and an NSURLSession
or an upload task:
NSData *compressedData = _gzipCompressData(originalData, &err);
NSURLRequest *request = ...;
[request setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
[urlSession uploadTaskWithRequest:request fromData:compressedData];
With this you can shave a few percent off your upload sizes saving bandwidth and time!
Here are examples where I’m doing this in TJMixpanelLogger
and TJDropbox
.