Skip to content

Client closing connection results in crash for HTTP post multipart requests #96

Open
@kinafu

Description

@kinafu

Describe the bug

When a client aborts a request, the program hangs.

The sketch below has been adapted from the HTTP forms example.

Please use the sketch from the second post, it's simpler!

More complex sketch with multipart/form-data
/**
 * Example for the ESP32 HTTP(S) Webserver
 *
 * IMPORTANT NOTE:
 * To run this script, you need to
 *  1) Enter your WiFi SSID and PSK below this comment
 *  2) Make sure to have certificate data available. You will find a
 *     shell script and instructions to do so in the library folder
 *     under extras/
 *
 * This script will install an HTTPS Server on your ESP32 with the following
 * functionalities:
 *  - Show simple page on web server root that includes some HTML Forms
 *  - Define a POST handler that handles the forms using the HTTPBodyParser API
 *    provided by the library.
 *  - 404 for everything else
 */

// TODO: Configure your WiFi here
#define WIFI_SSID "<insert your WiFi>"
#define WIFI_PSK  "<insert your WiFi pass>"

// Include certificate data (see note above)

// We will use wifi
#include <WiFi.h>

// We will use SPIFFS and FS
#include <SPIFFS.h>
#include <FS.h>

// Includes for the server
#include <HTTPSServer.hpp>
#include <SSLCert.hpp>
#include <HTTPRequest.hpp>
#include <HTTPResponse.hpp>
#include <HTTPBodyParser.hpp>
#include <HTTPMultipartBodyParser.hpp>
#include <HTTPURLEncodedBodyParser.hpp>

// The HTTPS Server comes in a separate namespace. For easier use, include it here.
using namespace httpsserver;

// Create an SSL certificate object from the files included above
// SSLCert cert = SSLCert(
//   example_crt_DER, example_crt_DER_len,
//   example_key_DER, example_key_DER_len
// );

// Create an SSL-enabled server that uses the certificate
// The contstructor takes some more parameters, but we go for default values here.
SSLCert * cert;
HTTPSServer * secureServer;


// Declare some handler functions for the various URLs on the server
// See the static-page example for how handler functions work.
// The comments in setup() describe what each handler function does in this example.

void handleFormUpload(HTTPRequest * req, HTTPResponse * res);

void handleFormUpload(HTTPRequest * req, HTTPResponse * res) {
  // First, we need to check the encoding of the form that we have received.
  // The browser will set the Content-Type request header, so we can use it for that purpose.
  // Then we select the body parser based on the encoding.
  // Actually we do this only for documentary purposes, we know the form is going
  // to be multipart/form-data.
  HTTPBodyParser *parser;
  std::string contentType = req->getHeader("Content-Type");

  // The content type may have additional properties after a semicolon, for exampel:
  // Content-Type: text/html;charset=utf-8
  // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs
  // As we're interested only in the actual mime _type_, we strip everything after the
  // first semicolon, if one exists:
  size_t semicolonPos = contentType.find(";");
  if (semicolonPos != std::string::npos) {
    contentType = contentType.substr(0, semicolonPos);
  }

  // Now, we can decide based on the content type:
  if (contentType == "multipart/form-data") {
    parser = new HTTPMultipartBodyParser(req);
  } else {
    Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str());
    return;
  }

  res->println("<html><head><title>File Upload</title></head><body><h1>File Upload</h1>");

  // We iterate over the fields. Any field with a filename is uploaded.
  // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's
  // fields only a single time. The reason for this is that it allows you to handle large requests
  // which would not fit into memory.

  // parser->nextField() will move the parser to the next field in the request body (field meaning a
  // form field, if you take the HTML perspective). After the last field has been processed, nextField()
  // returns false and the while loop ends.
  while(parser->nextField()) {
    // For Multipart data, each field has three properties:
    // The name ("name" value of the <input> tag)
    // The filename (If it was a <input type="file">, this is the filename on the machine of the
    //   user uploading it)
    // The mime type (It is determined by the client. So do not trust this value and blindly start
    //   parsing files only if the type matches)
    std::string name = parser->getFieldName();
    std::string filename = parser->getFieldFilename();
    std::string mimeType = parser->getFieldMimeType();
    // We log all three values, so that you can observe the upload on the serial monitor:
    Serial.printf("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), mimeType.c_str());

    // With endOfField you can check whether the end of field has been reached or if there's
    // still data pending. With multipart bodies, you cannot know the field size in advance.
    
    size_t fileLength = 0;

    while (!parser->endOfField()) {
      byte buf[512];
      size_t readLength = parser->read(buf, 512);
      fileLength += readLength;
      // Do nothing with the buffer, to keep things simple
    }
  }
  res->println("</body></html>");
  delete parser;
}

void setup() {
  // For logging
  Serial.begin(115200);
  // Connect to WiFi
  Serial.println("Setting up WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PSK);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.print("Connected. IP=");
  Serial.println(WiFi.localIP());

  // Setup filesystem
  if (!SPIFFS.begin(true)) Serial.println("Mounting SPIFFS failed");
  cert = new SSLCert();
  int createCertResult = createSelfSignedCert(
    *cert,
    KEYSIZE_1024,
    "CN=myesp32.local,O=FancyCompany,C=DE",
    "20190101000000",
    "20300101000000"
  );
  if (createCertResult != 0) {
    Serial.printf("Cerating certificate failed. Error Code = 0x%02X, check SSLCert.hpp for details", createCertResult);
    while(true) delay(500);
  }
  secureServer = new HTTPSServer(cert);

  // For every resource available on the server, we need to create a ResourceNode
  // The ResourceNode links URL and HTTP method to a handler function

  // The handleFormUpload handler handles the file upload from the root node. As the form
  // is submitted via post, we need to specify that as handler method here:
  ResourceNode * nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);

  // Add all nodes to the server so they become accessible:

  secureServer->registerNode(nodeFormUpload);

  Serial.println("Starting server...");
  secureServer->start();
  if (secureServer->isRunning()) {
    Serial.println("Server ready.");
  }
}

void loop() {
  // This call will let the server do its work
  secureServer->loop();

  // Other code would go here...
  delay(1);
}

Make a POST request to the endpoint using a large enough file
I used the GUI tool insomnia for that.
You may also use curl or any other tool.
Just make sure to use a file large enough to abort the requests while the file has not been fully transmitted.

Stop the transmission midway.
e.g. by pressing Ctrl + C or clicking on Cancel in Insomnia

Expected Behavior
The server should stop processing the request and indicate that information e.g. by a method returning a boolean.

Actual Behavior
The server either sees transmisson of zero-length data
or does not complete the req->readBytes() method.

ESP32 Module
Please provide specifications of your module

  • RAM/PSRAM: 320kiB
  • Flash Size: 4MiB

Software

  • IDE and Version: PlatformIO 4.3.4
  • OS: Windows 10
  • Client used to access the server: Insomnia

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions