An asynchronous HTTP client library for ESP32 microcontrollers, built on top of AsyncTCP. This library provides a simple and efficient way to make HTTP requests without blocking your main program execution.
⚠️ HTTPS Warning: Real TLS/HTTPS is NOT implemented yet.https://URLs are rejected withHTTPS_NOT_SUPPORTED. Do not use this library for sensitive data until TLS support is added.
- âś… Asynchronous HTTP requests - Non-blocking HTTP operations
- âś… Multiple HTTP methods - GET, POST, PUT, DELETE, HEAD, PATCH support
- âś… Custom headers - Set global and per-request headers
- âś… Callback-based responses - Success and error callbacks
- ✅ ESP32 only – (ESP8266 support removed since 1.0.1)
- âś… Simple API - Easy to use with minimal setup
- âś… Configurable timeouts - Set custom timeout values
- âś… Multiple simultaneous requests - Handle multiple requests concurrently
- âś… Chunked transfer decoding - Validates framing and exposes parsed trailers
- âś… Optional redirect following - Follow 301/302/303 (converted to GET) and 307/308 (method preserved)
- âś… Header & body guards - Limit buffered response headers/body to avoid runaway responses
âš Limitations: HTTPS not implemented; full body is buffered in memory (no zero-copy streaming yet).
Add to your platformio.ini:
lib_deps =
https://github.com/ESP32Async/AsyncTCP.git
https://github.com/playmiel/ESPAsyncWebClient.git- Download this repository as ZIP
- In Arduino IDE: Sketch → Include Library → Add .ZIP Library
- Install the dependencies:
- For ESP32: AsyncTCP by ESP32Async
#include <WiFi.h>
#include <ESPAsyncWebClient.h>
AsyncHttpClient client;
void setup() {
Serial.begin(115200);
// Connect to WiFi
WiFi.begin("your-ssid", "your-password");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
}
// Make a simple GET request
client.get("http://httpbin.org/get",
[](AsyncHttpResponse* response) {
Serial.printf("Success! Status: %d\n", response->getStatusCode());
Serial.printf("Body: %s\n", response->getBody().c_str());
},
[](HttpClientError error, const char* message) {
Serial.printf("Error: %s (%d)\n", httpClientErrorToString(error), (int)error);
}
);
}
void loop() {
#if !ASYNC_TCP_HAS_TIMEOUT
// If your AsyncTCP does NOT provide native timeouts, you must drive timeouts manually
// unless you build with -DASYNC_HTTP_ENABLE_AUTOLOOP (ESP32 only).
// Either:
// - Define ASYNC_HTTP_ENABLE_AUTOLOOP (ESP32): a tiny FreeRTOS task will call client.loop() for you; or
// - Call client.loop() periodically here yourself (recommended every ~10-20ms when busy).
// client.loop();
#endif
}On ESP32, if AsyncTCP lacks native timeout support, you have two options:
- Define
-DASYNC_HTTP_ENABLE_AUTOLOOP: the library creates a tiny FreeRTOS task that periodically callsclient.loop()in the background. This is convenient but introduces a background task; keep callbacks short. - Do not define it: call
client.loop()periodically yourself from your sketchloop()to drive timeouts.
If ASYNC_TCP_HAS_TIMEOUT is available in your AsyncTCP, neither is required for timeouts, but calling
client.loop() remains harmless.
// GET request
void get(const char* url, SuccessCallback onSuccess, ErrorCallback onError = nullptr);
// POST request with data
void post(const char* url, const char* data, SuccessCallback onSuccess, ErrorCallback onError = nullptr);
// PUT request with data
void put(const char* url, const char* data, SuccessCallback onSuccess, ErrorCallback onError = nullptr);
// DELETE request
uint32_t del(const char* url, SuccessCallback onSuccess, ErrorCallback onError = nullptr);
// HEAD request
uint32_t head(const char* url, SuccessCallback onSuccess, ErrorCallback onError = nullptr);
// PATCH request (with data)
uint32_t patch(const char* url, const char* data, SuccessCallback onSuccess, ErrorCallback onError = nullptr);
// Advanced request (custom method, headers, streaming, etc.)
uint32_t request(AsyncHttpRequest* request, SuccessCallback onSuccess, ErrorCallback onError = nullptr);
// Abort a request by its ID
bool abort(uint32_t requestId);// Set global default header
void setHeader(const char* name, const char* value);
void removeHeader(const char* name);
void clearHeaders();
// Set total request timeout (milliseconds)
void setTimeout(uint32_t timeout);
// Set connect phase timeout distinct from total timeout
void setDefaultConnectTimeout(uint32_t ms);
// Follow HTTP redirects (max hops clamps to >=1). Disabled by default.
void setFollowRedirects(bool enable, uint8_t maxHops = 3);
// Abort if response headers exceed this many bytes (0 = unlimited)
void setMaxHeaderBytes(size_t maxBytes);
// Soft limit for buffered response bodies (bytes, 0 = unlimited)
void setMaxBodySize(size_t maxBytes);
// Limit simultaneous active requests (0 = unlimited, others queued)
void setMaxParallel(uint16_t maxParallel);
// Set User-Agent string
void setUserAgent(const char* userAgent);typedef std::function<void(AsyncHttpResponse*)> SuccessCallback;
typedef std::function<void(HttpClientError, const char*)> ErrorCallback;// Response status
int getStatusCode() const;
const String& getStatusText() const;
// Response headers
const String& getHeader(const String& name) const;
const std::vector<HttpHeader>& getHeaders() const;
const String& getTrailer(const String& name) const;
const std::vector<HttpHeader>& getTrailers() const;
// Response body
const String& getBody() const;
size_t getContentLength() const;
// Status helpers
bool isSuccess() const; // 2xx status codes
bool isRedirect() const; // 3xx status codes
bool isError() const; // 4xx+ status codesExample of reading decoded chunk trailers:
client.get("http://example.com/chunked", [](AsyncHttpResponse* response) {
for (const auto& trailer : response->getTrailers()) {
Serial.printf("Trailer %s: %s\n", trailer.name.c_str(), trailer.value.c_str());
}
});// Create custom request
AsyncHttpRequest request(HTTP_POST, "http://example.com/api");
// Set headers
request.setHeader("Content-Type", "application/json");
request.setHeader("Authorization", "Bearer token");
request.removeHeader("Accept-Encoding");
// Set body
request.setBody("{\"key\":\"value\"}");
// Set timeout
request.setTimeout(10000);
// Execute
client.request(&request, onSuccess, onError);client.get("http://api.example.com/data",
[](AsyncHttpResponse* response) {
if (response->isSuccess()) {
Serial.println("Data received:");
Serial.println(response->getBody());
}
}
);client.setHeader("Content-Type", "application/json");
String jsonData = "{\"sensor\":\"temperature\",\"value\":25.5}";
client.post("http://api.example.com/sensor", jsonData.c_str(),
[](AsyncHttpResponse* response) {
Serial.printf("Posted data, status: %d\n", response->getStatusCode());
}
);// These requests will be made concurrently
client.get("http://api1.example.com/data", onSuccess1);
client.get("http://api2.example.com/data", onSuccess2);
client.post("http://api3.example.com/data", "payload", onSuccess3);// Set global headers (applied to all requests)
client.setHeader("X-API-Key", "your-api-key");
client.setUserAgent("MyDevice/1.0");
// Or set per-request headers
AsyncHttpRequest* request = new AsyncHttpRequest(HTTP_GET, "http://example.com");
request->setHeader("Authorization", "Bearer token");
client.request(request, onSuccess);client.setFollowRedirects(true, 3); // follow at most 3 hops
client.post("http://example.com/login", "user=demo", [](AsyncHttpResponse* response) {
Serial.printf("Final location responded with %d\n", response->getStatusCode());
});- 301/302/303 responses switch to
GETautomatically (body dropped). - 307/308 keep the original method and body (stream bodies cannot be replayed automatically).
- Sensitive headers (
Authorization,Proxy-Authorization) are stripped when the redirect crosses hosts. - Redirects are triggered as soon as the headers arrive; the client skips downloading any subsequent 3xx body data.
See examples/arduino/NoStoreToSD/NoStoreToSD.ino for a full download example using setNoStoreBody(true) and a global onBodyChunk handler that streams chunked and non-chunked responses to an SD card.
Error codes passed to error callbacks: see the single authoritative table in the “Error Codes” section below.
client.get("http://example.com", onSuccess,
[](HttpClientError error, const char* message) {
switch(error) {
case CONNECTION_FAILED:
Serial.println("Connection failed");
break;
case REQUEST_TIMEOUT:
Serial.println("Request timed out");
break;
default:
Serial.printf("Network error: %s (%d)\n", httpClientErrorToString(error), (int)error);
}
}
);// Set default timeout for all requests (10 seconds)
client.setTimeout(10000);
// Set default User-Agent
client.setUserAgent("ESP32-IoT-Device/1.0");
// Set default headers applied to all requests
client.setHeader("X-Device-ID", "esp32-001");
client.setHeader("Accept", "application/json");AsyncHttpRequest* request = new AsyncHttpRequest(HTTP_POST, url);
request->setTimeout(30000); // 30 second timeout for this request
request->setHeader("Content-Type", "application/xml");
request->setBody(xmlData);- The library automatically manages memory for standard requests
- For advanced
AsyncHttpRequestobjects, the library takes ownership and will delete them - Response objects are automatically cleaned up after callbacks complete
- No manual memory management required for typical usage
IMPORTANT: The
AsyncHttpResponse*pointer passed to the success callback is ONLY valid during that callback. Do not store it or references to its internalStringobjects. Copy what you need.
Register a global streaming callback via:
client.onBodyChunk([](const char* data, size_t len, bool final) {
// data may be nullptr & len==0 when final==true and no trailing bytes
});Parameters:
data,len: received segment (for chunked: decoded chunk payload; for non-chunked: raw slice). Whenfinal==trueand no extra bytes,datacan benullptr.final: true when the whole response body is complete.
Notes:
- Invoked for every segment (chunk or contiguous data block)
- The full body is still accumulated internally (future option may allow disabling accumulation)
finalis invoked just before the success callback- Keep it lightweight (avoid blocking operations)
If Content-Length is present, the response is considered complete once that many bytes have been received. Extra bytes (if a misbehaving server sends more) are ignored. Without Content-Length, completion is determined by connection close.
Configure client.setMaxBodySize(maxBytes) to abort early when the announced Content-Length or accumulated chunk data would exceed maxBytes, yielding MAX_BODY_SIZE_EXCEEDED. Pass 0 (default) to disable the guard.
Likewise, guard against oversized or malicious header blocks via client.setMaxHeaderBytes(limit). When the cumulative response headers exceed limit bytes before completion of \r\n\r\n, the request aborts with HEADERS_TOO_LARGE.
Chunked decoding validates frame boundaries and parses trailer headers for attachment to the response object.
Highlights / limitations:
- Trailer headers are parsed during chunked responses and available via
AsyncHttpResponse::getTrailers() - Chunk extensions are ignored but accepted
- Strict CRLF framing is required; malformed chunks raise
CHUNKED_DECODE_FAILED
https:// URLs return HTTPS_NOT_SUPPORTED. To add TLS later, wrap or replace AsyncClient with a secure implementation.
- The library is designed for single-threaded use (Arduino main loop)
- Callbacks are executed in the context of the network event loop
- Keep callback functions lightweight and non-blocking
- ESP32: AsyncTCP by ESP32Async
- Arduino Core: ESP32 (v2.0+)
Note: ESP8266 was mentioned in early docs but is no longer supported as of 1.0.1. The code exclusively targets AsyncTCP (ESP32).
- Current target: ESP32 only
- ESP8266: removed (no conditional code path retained)
- No TLS (HTTPS rejected)
- Chunked: trailers parsed and attached to
AsyncHttpResponse::getTrailers() - Full in-memory buffering (guard with
setMaxBodySizeor use no-store + chunk callback) - Redirects disabled by default; opt-in via
client.setFollowRedirects(...) - No long-lived keep-alive: default header
Connection: close; no connection reuse currently. - Manual timeout loop required if AsyncTCP version lacks
setTimeout(callclient.loop()inloop()). - No specific content-encoding handling (gzip/deflate ignored if sent).
AsyncHttpClient::makeRequest()creates a dynamicAsyncHttpRequest(or you pass yours torequest()).request()allocates aRequestContext, anAsyncHttpResponseand anAsyncClient.- Once connected the fully built HTTP request is written (
buildHttpRequest()). - Reception: headers buffered until
\r\n\r\n, then body accumulation (or chunk decoding). - On complete success: success callback invoked with
AsyncHttpResponse*(valid only during the callback). - On error or after success callback returns:
cleanup()deletesAsyncClient,AsyncHttpRequest,AsyncHttpResponse,RequestContext. - Do not keep any pointer/reference after callback return (it will dangle).
For very large bodies or future streaming options, a hook would be placed inside handleData after headersComplete before appendBody.
Single authoritative list (kept in sync with HttpCommon.h):
| Code | Enum | Meaning |
|---|---|---|
| -1 | CONNECTION_FAILED | Failed to initiate TCP connection or transport error mapped from AsyncTCP |
| -2 | HEADER_PARSE_FAILED | Invalid HTTP response headers |
| -3 | CONNECTION_CLOSED | Connection closed before headers received |
| -4 | REQUEST_TIMEOUT | Total request timeout exceeded |
| -5 | HTTPS_NOT_SUPPORTED | HTTPS not supported yet |
| -6 | CHUNKED_DECODE_FAILED | Failed to decode chunked body |
| -7 | CONNECT_TIMEOUT | Connect phase timeout |
| -8 | BODY_STREAM_READ_FAILED | Body streaming provider failed |
| -9 | ABORTED | Aborted by user |
| -10 | CONNECTION_CLOSED_MID_BODY | Connection closed after headers with body still missing bytes (truncated body) |
| -11 | MAX_BODY_SIZE_EXCEEDED | Body exceeds configured maximum (setMaxBodySize) |
| -12 | TOO_MANY_REDIRECTS | Redirect chain exceeded configured hop limit (setFollowRedirects) |
| -13 | HEADERS_TOO_LARGE | Response headers exceeded configured limit (setMaxHeaderBytes) |
| >0 | (AsyncTCP) | Not used: transport errors are mapped to CONNECTION_FAILED |
Example mapping in a callback:
client.get("http://example.com",
[](AsyncHttpResponse* r) {
Serial.printf("OK %d %s\n", r->getStatusCode(), r->getStatusText().c_str());
},
[](HttpClientError e, const char* msg) {
switch (e) {
case CONNECTION_FAILED: Serial.println("TCP connect failed"); break;
case HEADER_PARSE_FAILED: Serial.println("Bad HTTP header"); break;
case CONNECTION_CLOSED: Serial.println("Closed before headers"); break;
case CONNECTION_CLOSED_MID_BODY: Serial.println("Body truncated (closed mid-body)"); break;
case REQUEST_TIMEOUT: Serial.println("Timeout"); break;
case MAX_BODY_SIZE_EXCEEDED: Serial.println("Body exceeded guard"); break;
case TOO_MANY_REDIRECTS: Serial.println("Redirect loop detected"); break;
case HEADERS_TOO_LARGE: Serial.println("Headers exceeded guard"); break;
default: Serial.printf("Network error: %s (%d)\n", httpClientErrorToString(e), (int)e); break;
}
}
);To test compatibility with different versions of AsyncTCP, use the provided test script:
./scripts/test-dependencies.shThis script tests compilation with:
- AsyncTCP ESP32Async/main (development)
- AsyncTCP ESP32Async stable
You can also test individual environments:
# Test with development AsyncTCP
pio run -e esp32dev_asynctcp_dev
# Test with stable AsyncTCP
pio run -e test_asynctcp_stable
# Basic compilation test
pio run -e compile_test
# Chunk decoder regression tests
pio test -e esp32dev -f test_chunk_parseThis project is licensed under the MIT License - see the LICENSE file for details.
- Added: HEAD, PATCH
Contributions are welcome! Please feel free to submit a Pull Request.
- Streaming request body (no-copy) via setBodyStream
- Global body chunk callback (per-request callback removed for API simplicity)
- Basic Auth helper (request->setBasicAuth)
- Query param builder (addQueryParam/finalizeQueryParams)
- Optional Accept-Encoding: gzip (no automatic decompression yet)
- Separate connect timeout and total timeout
- Optional request queue limiting parallel connections (setMaxParallel)
- Soft response buffering guard (
setMaxBodySize) to fail fast on oversized payloads - Request ID return (all helper methods now return a uint32_t identifier)
- No-store body mode:
req->setNoStoreBody(true)to avoid buffering body when a chunk callback is used (final(nullptr, 0, true)event fired once)
Current: only the Accept-Encoding: gzip header can be added via enableGzipAcceptEncoding(true).
The library DOES NOT yet decompress gzip payloads. If you don't want compressed responses, simply don't enable the header.
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.
A future optional flag (ASYNC_HTTP_ENABLE_GZIP_DECODE) may add a tiny inflater (miniz/zlib) after flash/RAM impact is evaluated.
HTTPS is not implemented. Any https:// URL returns HTTPS_NOT_SUPPORTED. A future drop-in TLS client (replacing AsyncClient) is planned without breaking the public API.
See Arduino sketch at examples/arduino/StreamingUpload/StreamingUpload.ino or the PlatformIO project at examples/platformio/StreamingUpload/src/main.cpp for a streaming (no-copy) upload demonstrating:
-
setBodyStream() -
Basic Auth (
setBasicAuth) -
Query params builder (
addQueryParam/finalizeQueryParams) -
Connection limiting (
setMaxParallel) -
Create an issue on GitHub for bug reports or feature requests
-
Check the examples directory for usage patterns
-
Review the API documentation above for detailed information
See the GitHub Releases page for version history and changes.