Async TCP http download working example

Is there any Async TCP HTTP download that actually works on ESP32... that have a working example and not throwing panic attacks at random events?

I am not a programming wizard and I can't run around in circles with chatgpt and grok

Hi @borquee !

You wrote "Async TCP HTTP download", so you are talking about a TCP client? Could you perhaps say a little more about your intended application?

ec2021

Need to download images of the local web server (over wifi) and display them on a small screen … periodically … images are about 50-100kb jpegs … am all fine with displaying

Ok.

While preparing to answer you I just realised that the libraries I used last year to create an ESP32 web server (used for up to 10 users to input A, B, C, D answers for a quiz on a PC) throw the error you mentioned :wink:

However I have found this 1:1 replacements and tested them successfully with my application:

https://github.com/ESP32Async/AsyncTCP

https://github.com/ESP32Async/ESPAsyncWebServer

There is an example for a TCP Client

https://github.com/ESP32Async/AsyncTCP/blob/main/examples/Client/Client.ino

This would be the place where data are received

    client->onData([](void *arg, AsyncClient *client, void *data, size_t len) {
      Serial.printf("** data received by client: %" PRIu16 ": len=%u\n", client->localPort(), len);
    });

and where you could copy them to your display.

The example creates a number of clients (MAX_CLIENTS) which you would not require.

Good luck!
ec2021

The libs are available via library manager:


I think you might only need to install the AsyncTCP ...

@borquee

Maybe these might help...

And there is also an example how to fetch a Website ... That should be close to your needs:

https://github.com/ESP32Async/AsyncTCP/tree/main/examples/FetchWebsite

Lucky: that example was added ten days ago.

Note usage notes in the README to avoid crashes when using AsyncTCP, which require changing some settings, probably using platform.local.txt

There are plenty of trivial one-connection-at-a-time web servers with ESP32; being async makes sense if you can't control when clients contact you with potentially overlapping requests.

But as a client, periodically downloading 100KB images locally: what else is your board doing? Would it block too long? With ESP32, you might try doing that in a separate task. You could even do a loop-driven stateful download in the main task. No extra async library would be needed. Unlikely to panic; if it fails, it would be in more mundane ways.

It has an lvgl scrolling text, status text, slider, background image and on top of all that espnow connection … all working fine except download

I'm just gonna leave it here ...
Downloads multiple files at once ... didn't crash once :slight_smile:
(Use that configuration changes just to be safe!)

// Structure to hold download state
struct DownloadContext {
  AsyncClient* client;
  File file;
  String host;
  String path;
  String filename;
  String saveFilename;
  int port;
  bool headerParsed;
  int contentLength;
  int downloadedBytes;
  int tip;
};

void downloadFile(const char* cUrl, int tip) {
  // Ensure SPIFFS is mounted
  if (!SPIFFS.begin(true)) {
    Serial.println("SPIFFS Mount Failed");
    return;
  }

  //parse url
  String url = String(cUrl);
  String host;
  int port;
  String path;
  String filename;

  //parse URL

  // Find the "://" to skip the protocol (e.g., "http://")
  int protocolIndex = url.indexOf("://");
  int startIndex = (protocolIndex == -1) ? 0 : protocolIndex + 3;

  // Find the first '/' after the protocol; this starts the path.
  int pathIndex = url.indexOf("/", startIndex);

  // If no slash is found, treat everything after protocol as the host.
  if (pathIndex == -1) {
    host = url.substring(startIndex);
    port = 80;  // default port
    path = "";
    filename = "";
    return;
  }

  // Extract host (and optional port) from the URL.
  String hostPort = url.substring(startIndex, pathIndex);

  // Check if a port is specified with a colon.
  int colonIndex = hostPort.indexOf(":");
  if (colonIndex != -1) {
    host = hostPort.substring(0, colonIndex);
    String portString = hostPort.substring(colonIndex + 1);
    port = portString.toInt();
  } else {
    host = hostPort;
    port = 80;  // default port if not specified
  }

  // The path starts from the first '/' after the host.
  path = url.substring(pathIndex);

  // The filename is the substring after the last '/'.
  int lastSlash = url.lastIndexOf("/");
  if (lastSlash != -1 && lastSlash < url.length() - 1) {
    filename = url.substring(lastSlash + 1);
  } else {
    filename = "";
  }

  // Allocate our context
  DownloadContext* ctx = new DownloadContext;
  ctx->host = host;
  ctx->port = port;
  ctx->path = path;
  ctx->filename = filename;
  ctx->saveFilename = "/" + filename;
  ctx->headerParsed = false;
  ctx->tip = tip;
  ctx->downloadedBytes = 0;

  // Open file for writing
  ctx->file = SPIFFS.open(ctx->saveFilename, FILE_WRITE);
  if (!ctx->file) {
    Serial.println("Failed to open file for writing");
    delete ctx;
    return;
  }

  // Create AsyncTCP client
  AsyncClient* client = new AsyncClient();
  if (!client) {
    Serial.println("Unable to allocate client");
    ctx->file.close();
    delete ctx;
    return;
  }
  ctx->client = client;

  // onConnect: send the HTTP GET request once connected
  client->onConnect([](void* arg, AsyncClient* client) {
    DownloadContext* ctx = (DownloadContext*)arg;
    //    Serial.println("Connected to server");
    // Build HTTP GET request
    char request[256];
    snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: ESP32\r\nConnection: close\r\n\r\n", ctx->path.c_str(), ctx->host.c_str());
    client->write(request, strlen(request));
  },
                    ctx);

  // onData: receive data, skip the HTTP header and write the body to file
  client->onData([](void* arg, AsyncClient* client, void* data, size_t len) {
    DownloadContext* ctx = (DownloadContext*)arg;
    uint8_t* buf = (uint8_t*)data;
    if (!ctx->headerParsed) {
      // Look for the end of the HTTP header ("\r\n\r\n")
      for (size_t i = 0; i < len - 3; i++) {
        if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n') {
          size_t headerEnd = i + 4;
          ctx->headerParsed = true;

          // Extract the header into a String for parsing
          String header = "";
          for (size_t j = 0; j < headerEnd; j++) {
            header += (char)buf[j];
          }

          // Parse HTTP status code (e.g., "HTTP/1.1 200 OK")
          int firstSpace = header.indexOf(' ');
          int secondSpace = header.indexOf(' ', firstSpace + 1);
          int statusCode = -1;
          if (firstSpace != -1 && secondSpace != -1) {
            String codeStr = header.substring(firstSpace + 1, secondSpace);
            statusCode = codeStr.toInt();
          }

          // If status code is not 200, stop everything and delete the file
          if (statusCode != 200) {
            Serial.printf("HTTP Error: %d. Aborting download and deleting file.\n", statusCode);
            client->close();
            ctx->file.close();
            SPIFFS.remove(ctx->saveFilename.c_str());
            return;
          }

          // Optionally, parse Content-Length (if needed)
          int contentLength = -1;
          int contentIndex = header.indexOf("Content-Length:");
          if (contentIndex != -1) {
            int colonIndex = header.indexOf(":", contentIndex);
            int lineEnd = header.indexOf("\r\n", colonIndex);
            if (colonIndex != -1 && lineEnd != -1) {
              String clStr = header.substring(colonIndex + 1, lineEnd);
              clStr.trim();
              contentLength = clStr.toInt();
              ctx->contentLength = contentLength;
            }
          }

          // Write any remaining bytes (body) to the file
          int bodyLen = len - headerEnd;
          ctx->file.write(buf + headerEnd, bodyLen);
          ctx->downloadedBytes += bodyLen;
          return;
        }
      }
    } else {
      // Header already parsed: write all subsequent data to the file.
      ctx->file.write(buf, len);
      ctx->downloadedBytes += len;
    }
  },
                 ctx);

  // onDisconnect: clean up once the connection is closed
  client->onDisconnect([](void* arg, AsyncClient* client) {
    DownloadContext* ctx = (DownloadContext*)arg;
    ctx->file.close();

    Serial.printf("Download complete: %d bytes received.\n", ctx->downloadedBytes);
    bool fileDeleted = false;
    if (ctx->contentLength != -1) {
      if (ctx->downloadedBytes == ctx->contentLength) {
        Serial.println("Downloaded data matches Content-Length.");

        //do your thing ...
        Serial.printf("ctx->tip %d\n", ctx->tip);
      } else {
        Serial.printf("Mismatch: Downloaded %d bytes but expected %d bytes. Deleting file.\n",
                      ctx->downloadedBytes, ctx->contentLength);
        SPIFFS.remove(ctx->saveFilename.c_str());
        fileDeleted = true;
      }
    } else {
      Serial.println("Content-Length header was not provided; cannot verify download size.");
    }

    if (!fileDeleted) {
      Serial.printf("File saved as %s\n", ctx->saveFilename.c_str());
    }

    delete ctx;
    delete client;
  },
                       ctx);

  // onError: handle errors by closing the client and cleaning up
  client->onError([](void* arg, AsyncClient* client, int8_t error) {
    Serial.print("Connection error: ");
    Serial.println(error);
    client->close();
  },
                  ctx);

  // Start the connection using the resolved IP address
  if (!client->connect(ctx->host.c_str(), ctx->port)) {
    Serial.println("Connection failed");
    ctx->file.close();
    delete ctx;
    delete client;
  }
}