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
âš 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); // 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 codes// 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);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.
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) - No automatic redirects (3xx not followed)
- 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) |
| >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; 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.