Skip to content

Commit e469f4b

Browse files
committed
add gzip
1 parent 5f19e37 commit e469f4b

14 files changed

Lines changed: 1847 additions & 44 deletions

File tree

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ Common HTTPS errors:
468468
- Redirects disabled by default; opt-in via `client.setFollowRedirects(...)`
469469
- No long-lived keep-alive: default header `Connection: close`; no connection reuse currently.
470470
- Manual timeout loop required if AsyncTCP version lacks `setTimeout` (call `client.loop()` in `loop()`).
471-
- No specific content-encoding handling (gzip/deflate ignored if sent).
471+
- No general content-encoding handling (br/deflate not supported); optional `gzip` decode is available via `ASYNC_HTTP_ENABLE_GZIP_DECODE`.
472472

473473
## Object lifecycle / Ownership
474474

@@ -505,6 +505,7 @@ Single authoritative list (kept in sync with `HttpCommon.h`):
505505
| -15 | TLS_CERT_INVALID | TLS certificate validation failed |
506506
| -16 | TLS_FINGERPRINT_MISMATCH | TLS fingerprint pinning rejected the peer certificate |
507507
| -17 | TLS_HANDSHAKE_TIMEOUT | TLS handshake exceeded the configured timeout |
508+
| -18 | GZIP_DECODE_FAILED | Failed to decode gzip body (`Content-Encoding: gzip`) |
508509
| >0 | (AsyncTCP) | Not used: transport errors are mapped to CONNECTION_FAILED |
509510

510511
Example mapping in a callback:
@@ -577,7 +578,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
577578
- Global body chunk callback (per-request callback removed for API simplicity)
578579
- Basic Auth helper (request->setBasicAuth)
579580
- Query param builder (addQueryParam/finalizeQueryParams)
580-
- Optional Accept-Encoding: gzip (no automatic decompression yet)
581+
- Optional Accept-Encoding: gzip (+ optional transparent decode via `ASYNC_HTTP_ENABLE_GZIP_DECODE`)
581582
- Separate connect timeout and total timeout
582583
- Optional request queue limiting parallel connections (setMaxParallel)
583584
- Soft response buffering guard (`setMaxBodySize`) to fail fast on oversized payloads
@@ -586,11 +587,16 @@ Contributions are welcome! Please feel free to submit a Pull Request.
586587

587588
### Gzip / Compression
588589

589-
Current: only the `Accept-Encoding: gzip` header can be added via `enableGzipAcceptEncoding(true)`.
590-
The library DOES NOT yet decompress gzip payloads. If you don't want compressed responses, simply don't enable the header.
590+
Default: only the `Accept-Encoding: gzip` header can be added via `enableGzipAcceptEncoding(true)`.
591591

592-
Important: calling `enableGzipAcceptEncoding(false)` does not remove the header if it was already added earlier on the same request instance. Create a new request without enabling it to avoid sending the header.
593-
A future optional flag (`ASYNC_HTTP_ENABLE_GZIP_DECODE`) may add a tiny inflater (miniz/zlib) after flash/RAM impact is evaluated.
592+
Optional decode: build with `-DASYNC_HTTP_ENABLE_GZIP_DECODE=1` to transparently inflate `Content-Encoding: gzip` responses (both in-memory body and `client.onBodyChunk(...)` stream).
593+
594+
Notes:
595+
596+
- If you don't want compressed responses, simply don't enable the header.
597+
- `enableGzipAcceptEncoding(false)` removes `Accept-Encoding` from the request's header list (or call `request.removeHeader("Accept-Encoding")`).
598+
- `Content-Length` (when present) refers to the *compressed* payload size; completion detection still follows the wire length.
599+
- RAM impact: enabling gzip decode allocates an internal 32KB sliding window per active gzip-decoded response (plus small state).
594600

595601
### HTTPS quick reference
596602

platformio.ini

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ build_src_filter = -<*> +<../src/> +<../test/compile_test_internal/compile_test.
3838
lib_deps =
3939
https://github.com/ESP32Async/AsyncTCP.git
4040

41+
[env:compile_test_gzip]
42+
platform = espressif32
43+
board = esp32dev
44+
framework = arduino
45+
platform_packages =
46+
framework-arduinoespressif32@^3
47+
build_flags =
48+
${env.build_flags}
49+
-DCOMPILE_TEST_ONLY
50+
-DASYNC_HTTP_ENABLE_GZIP_DECODE=1
51+
build_src_filter = -<*> +<../src/> +<../test/compile_test_internal/compile_test.cpp>
52+
lib_deps =
53+
https://github.com/ESP32Async/AsyncTCP.git
54+
4155

4256
# Environment for testing with different AsyncTCP versions
4357
[env:esp32dev_asynctcp_dev]
@@ -74,6 +88,7 @@ platform = native
7488
; Only build the standalone URL parser for host tests to avoid Arduino deps
7589
; Do not compile Arduino-based tests in native
7690
test_ignore = test_parse_url, test_chunk_parse, test_redirects, test_cookies, test_keep_alive
77-
build_src_filter = -<*> +<UrlParser.cpp>
91+
build_src_filter = -<*> +<UrlParser.cpp> +<GzipDecoder.cpp> +<third_party/miniz/miniz_tinfl.c>
7892
build_flags =
7993
-I test/test_urlparser_native
94+
-I src

src/AsyncHttpClient.cpp

Lines changed: 172 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -651,20 +651,99 @@ void AsyncHttpClient::handleConnect(RequestContext* context) {
651651
}
652652

653653
void AsyncHttpClient::handleData(RequestContext* context, char* data, size_t len) {
654-
bool storeBody = context && context->request && !context->request->getNoStoreBody();
655-
bool bufferThisChunk = context && (!context->headersComplete || context->chunked);
654+
if (!context)
655+
return;
656+
bool storeBody = context->request && !context->request->getNoStoreBody();
657+
bool bufferThisChunk = (!context->headersComplete || context->chunked);
656658
if (bufferThisChunk)
657659
context->responseBuffer.concat(data, len);
658660
bool enforceLimit = shouldEnforceBodyLimit(context);
659-
auto wouldExceedLimit = [&](size_t incoming) -> bool {
661+
auto wouldExceedBodyLimit = [&](size_t incoming) -> bool {
660662
if (!enforceLimit)
661663
return false;
662-
size_t current = context->receivedContentLength;
664+
size_t current = context->receivedBodyLength;
663665
if (current >= _maxBodySize)
664666
return true;
665667
return incoming > (_maxBodySize - current);
666668
};
667669

670+
auto emitBodyBytes = [&](const char* out, size_t outLen) -> bool {
671+
if (!out || outLen == 0)
672+
return true;
673+
if (wouldExceedBodyLimit(outLen)) {
674+
triggerError(context, MAX_BODY_SIZE_EXCEEDED, "Body exceeds configured maximum");
675+
return false;
676+
}
677+
if (storeBody) {
678+
context->response->appendBody(out, outLen);
679+
}
680+
context->receivedBodyLength += outLen;
681+
auto cb = _bodyChunkCallback;
682+
if (cb)
683+
cb(out, outLen, false);
684+
return true;
685+
};
686+
687+
auto deliverWireBytes = [&](const char* wire, size_t wireLen) -> bool {
688+
if (wireLen == 0)
689+
return true;
690+
context->receivedContentLength += wireLen;
691+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
692+
if (context->gzipDecodeActive) {
693+
size_t offset = 0;
694+
while (offset < wireLen) {
695+
const uint8_t* outPtr = nullptr;
696+
size_t outLen = 0;
697+
size_t consumed = 0;
698+
GzipDecoder::Result r = context->gzipDecoder.write(reinterpret_cast<const uint8_t*>(wire + offset),
699+
wireLen - offset, &consumed, &outPtr, &outLen, true);
700+
if (outLen > 0) {
701+
if (!emitBodyBytes(reinterpret_cast<const char*>(outPtr), outLen))
702+
return false;
703+
}
704+
if (r == GzipDecoder::Result::kError) {
705+
triggerError(context, GZIP_DECODE_FAILED, context->gzipDecoder.lastError());
706+
return false;
707+
}
708+
offset += consumed;
709+
if (consumed == 0 && outLen == 0) {
710+
triggerError(context, GZIP_DECODE_FAILED, "Gzip decoder stalled");
711+
return false;
712+
}
713+
if (r == GzipDecoder::Result::kNeedMoreInput && offset >= wireLen) {
714+
break;
715+
}
716+
}
717+
return true;
718+
}
719+
#endif
720+
return emitBodyBytes(wire, wireLen);
721+
};
722+
723+
auto finalizeDecoding = [&]() -> bool {
724+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
725+
if (!context->gzipDecodeActive)
726+
return true;
727+
for (;;) {
728+
const uint8_t* outPtr = nullptr;
729+
size_t outLen = 0;
730+
GzipDecoder::Result r = context->gzipDecoder.finish(&outPtr, &outLen);
731+
if (outLen > 0) {
732+
if (!emitBodyBytes(reinterpret_cast<const char*>(outPtr), outLen))
733+
return false;
734+
}
735+
if (r == GzipDecoder::Result::kDone)
736+
return true;
737+
if (r == GzipDecoder::Result::kOk)
738+
continue;
739+
triggerError(context, GZIP_DECODE_FAILED, context->gzipDecoder.lastError());
740+
return false;
741+
}
742+
#else
743+
return true;
744+
#endif
745+
};
746+
668747
if (!context->headersComplete) {
669748
int headerEnd = context->responseBuffer.indexOf("\r\n\r\n");
670749
if (_maxHeaderBytes > 0) {
@@ -679,12 +758,17 @@ void AsyncHttpClient::handleData(RequestContext* context, char* data, size_t len
679758
String headerData = context->responseBuffer.substring(0, headerEnd);
680759
if (parseResponseHeaders(context, headerData)) {
681760
context->headersComplete = true;
682-
if (enforceLimit && context->expectedContentLength > 0 &&
761+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
762+
bool gzipActive = context->gzipDecodeActive;
763+
#else
764+
bool gzipActive = false;
765+
#endif
766+
if (enforceLimit && !gzipActive && context->expectedContentLength > 0 &&
683767
context->expectedContentLength > _maxBodySize) {
684768
triggerError(context, MAX_BODY_SIZE_EXCEEDED, "Body exceeds configured maximum");
685769
return;
686770
}
687-
if (storeBody && context->expectedContentLength > 0 && !context->chunked &&
771+
if (storeBody && !gzipActive && context->expectedContentLength > 0 && !context->chunked &&
688772
(!enforceLimit || context->expectedContentLength <= _maxBodySize)) {
689773
context->response->reserveBody(context->expectedContentLength);
690774
}
@@ -693,17 +777,12 @@ void AsyncHttpClient::handleData(RequestContext* context, char* data, size_t len
693777
return;
694778
if (!context->chunked && context->responseBuffer.length() > 0) {
695779
size_t incomingLen = context->responseBuffer.length();
696-
if (wouldExceedLimit(incomingLen)) {
780+
if (!gzipActive && wouldExceedBodyLimit(incomingLen)) {
697781
triggerError(context, MAX_BODY_SIZE_EXCEEDED, "Body exceeds configured maximum");
698782
return;
699783
}
700-
if (storeBody) {
701-
context->response->appendBody(context->responseBuffer.c_str(), incomingLen);
702-
}
703-
context->receivedContentLength += incomingLen;
704-
auto cb = _bodyChunkCallback;
705-
if (cb)
706-
cb(context->responseBuffer.c_str(), incomingLen, false);
784+
if (!deliverWireBytes(context->responseBuffer.c_str(), incomingLen))
785+
return;
707786
context->responseBuffer = "";
708787
}
709788
} else {
@@ -712,17 +791,17 @@ void AsyncHttpClient::handleData(RequestContext* context, char* data, size_t len
712791
}
713792
}
714793
} else if (!context->chunked) {
715-
if (wouldExceedLimit(len)) {
794+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
795+
bool gzipActive = context->gzipDecodeActive;
796+
#else
797+
bool gzipActive = false;
798+
#endif
799+
if (!gzipActive && wouldExceedBodyLimit(len)) {
716800
triggerError(context, MAX_BODY_SIZE_EXCEEDED, "Body exceeds configured maximum");
717801
return;
718802
}
719-
if (storeBody) {
720-
context->response->appendBody(data, len);
721-
}
722-
context->receivedContentLength += len;
723-
auto cb2 = _bodyChunkCallback;
724-
if (cb2)
725-
cb2(data, len, false);
803+
if (!deliverWireBytes(data, len))
804+
return;
726805
}
727806

728807
while (context->headersComplete && context->chunked && !context->chunkedComplete) {
@@ -796,7 +875,12 @@ void AsyncHttpClient::handleData(RequestContext* context, char* data, size_t len
796875
triggerError(context, CHUNKED_DECODE_FAILED, "Chunk size parse error");
797876
return;
798877
}
799-
if (chunkSize > 0 && wouldExceedLimit(chunkSize)) {
878+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
879+
bool gzipActive = context->gzipDecodeActive;
880+
#else
881+
bool gzipActive = false;
882+
#endif
883+
if (!gzipActive && chunkSize > 0 && wouldExceedBodyLimit(chunkSize)) {
800884
triggerError(context, MAX_BODY_SIZE_EXCEEDED, "Body exceeds configured maximum");
801885
return;
802886
}
@@ -817,18 +901,9 @@ void AsyncHttpClient::handleData(RequestContext* context, char* data, size_t len
817901
return;
818902
}
819903
size_t chunkLen = context->currentChunkRemaining;
820-
if (wouldExceedLimit(chunkLen)) {
821-
triggerError(context, MAX_BODY_SIZE_EXCEEDED, "Body exceeds configured maximum");
822-
return;
823-
}
824904
const char* chunkPtr = context->responseBuffer.c_str();
825-
if (storeBody) {
826-
context->response->appendBody(chunkPtr, chunkLen);
827-
}
828-
context->receivedContentLength += chunkLen;
829-
auto cb3 = _bodyChunkCallback;
830-
if (cb3)
831-
cb3(chunkPtr, chunkLen, false);
905+
if (!deliverWireBytes(chunkPtr, chunkLen))
906+
return;
832907
context->responseBuffer.remove(0, needed);
833908
context->currentChunkRemaining = 0;
834909
}
@@ -841,6 +916,8 @@ void AsyncHttpClient::handleData(RequestContext* context, char* data, size_t len
841916
context->receivedContentLength >= context->expectedContentLength)
842917
complete = true;
843918
if (complete) {
919+
if (!finalizeDecoding())
920+
return;
844921
processResponse(context);
845922
}
846923
}
@@ -893,6 +970,51 @@ void AsyncHttpClient::handleDisconnect(RequestContext* context) {
893970
triggerError(context, CONNECTION_CLOSED_MID_BODY, "Truncated response");
894971
return;
895972
}
973+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
974+
if (context->gzipDecodeActive) {
975+
bool storeBody = context->request && !context->request->getNoStoreBody();
976+
bool enforceLimit = shouldEnforceBodyLimit(context);
977+
auto wouldExceedBodyLimit = [&](size_t incoming) -> bool {
978+
if (!enforceLimit)
979+
return false;
980+
size_t current = context->receivedBodyLength;
981+
if (current >= _maxBodySize)
982+
return true;
983+
return incoming > (_maxBodySize - current);
984+
};
985+
auto emitBodyBytes = [&](const char* out, size_t outLen) -> bool {
986+
if (!out || outLen == 0)
987+
return true;
988+
if (wouldExceedBodyLimit(outLen)) {
989+
triggerError(context, MAX_BODY_SIZE_EXCEEDED, "Body exceeds configured maximum");
990+
return false;
991+
}
992+
if (storeBody) {
993+
context->response->appendBody(out, outLen);
994+
}
995+
context->receivedBodyLength += outLen;
996+
auto cb = _bodyChunkCallback;
997+
if (cb)
998+
cb(out, outLen, false);
999+
return true;
1000+
};
1001+
for (;;) {
1002+
const uint8_t* outPtr = nullptr;
1003+
size_t outLen = 0;
1004+
GzipDecoder::Result r = context->gzipDecoder.finish(&outPtr, &outLen);
1005+
if (outLen > 0) {
1006+
if (!emitBodyBytes(reinterpret_cast<const char*>(outPtr), outLen))
1007+
return;
1008+
}
1009+
if (r == GzipDecoder::Result::kDone)
1010+
break;
1011+
if (r == GzipDecoder::Result::kOk)
1012+
continue;
1013+
triggerError(context, GZIP_DECODE_FAILED, context->gzipDecoder.lastError());
1014+
return;
1015+
}
1016+
}
1017+
#endif
8961018
// Otherwise success: either Content-Length reached, or no Content-Length and closure marks the end
8971019
processResponse(context);
8981020
}
@@ -939,6 +1061,16 @@ bool AsyncHttpClient::parseResponseHeaders(RequestContext* context, const String
9391061
context->response->setContentLength(context->expectedContentLength);
9401062
} else if (name.equalsIgnoreCase("Transfer-Encoding") && value.equalsIgnoreCase("chunked")) {
9411063
context->chunked = true;
1064+
} else if (name.equalsIgnoreCase("Content-Encoding")) {
1065+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
1066+
String lower = value;
1067+
lower.toLowerCase();
1068+
if (lower.indexOf("gzip") != -1) {
1069+
context->gzipEncoded = true;
1070+
context->gzipDecodeActive = true;
1071+
context->gzipDecoder.begin();
1072+
}
1073+
#endif
9421074
} else if (name.equalsIgnoreCase("Connection")) {
9431075
String lower = value;
9441076
lower.toLowerCase();
@@ -1212,6 +1344,7 @@ void AsyncHttpClient::resetContextForRedirect(RequestContext* context, AsyncHttp
12121344
context->responseProcessed = false;
12131345
context->expectedContentLength = 0;
12141346
context->receivedContentLength = 0;
1347+
context->receivedBodyLength = 0;
12151348
context->chunked = false;
12161349
context->chunkedComplete = false;
12171350
context->currentChunkRemaining = 0;
@@ -1224,6 +1357,11 @@ void AsyncHttpClient::resetContextForRedirect(RequestContext* context, AsyncHttp
12241357
context->serverRequestedClose = false;
12251358
context->usingPooledConnection = false;
12261359
context->resolvedTlsConfig = AsyncHttpTLSConfig();
1360+
#if ASYNC_HTTP_ENABLE_GZIP_DECODE
1361+
context->gzipEncoded = false;
1362+
context->gzipDecodeActive = false;
1363+
context->gzipDecoder.reset();
1364+
#endif
12271365
#if !ASYNC_TCP_HAS_TIMEOUT
12281366
context->timeoutTimer = millis();
12291367
#endif

0 commit comments

Comments
 (0)