me-no-dev / ESPAsyncWebServer

Async Web Server for ESP8266 and ESP32
3.76k stars 1.22k forks source link

Document how to parse JSON POSTed to the ESP #195

Open probonopd opened 7 years ago

probonopd commented 7 years ago

How do we parse JSON POSTed to the ESP, i.e., what is the ESPAsyncWebServer equivalent of https://github.com/esp8266/Arduino/issues/1321#issuecomment-267676688

StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(server.arg("plain"));

?

The following educated guess, jsonBuffer.parseObject(request->getParam("plain", true)->value());, seems not to work for me:

  server.on("/api/printer/command", HTTP_POST, [](AsyncWebServerRequest * request) {
    const size_t bufferSize = JSON_OBJECT_SIZE(1) + 250;
    DynamicJsonBuffer jsonBuffer(bufferSize);
    JsonObject& root = jsonBuffer.parseObject(request->getParam("plain", true)->value());
    const char* command = root["command"];
    request->send(204);
  });

Is this documented somewhere? If not, please document it.

jeremypoulter commented 7 years ago

For what it is worth, I have got this to work, have a look here

probonopd commented 7 years ago

So you are saying it should be JsonObject& root = jsonBuffer.parseObject(request->getParam("body", true)->value());? Where do you have that information from?

jeremypoulter commented 7 years ago

I think so (was a while ago when I last looked at this). IIRC I just cut and paste the content of the not found handler from the example. This outputs loads of useful info about the request.

probonopd commented 7 years ago

Thank you. Can't seem to get it to work yet, though...

stelgenhof commented 7 years ago

Tried it and works perfectly!

probonopd commented 7 years ago

Apparently there is more than meets the eye. request->getParam("body", true)->value() does not work for me since apparently there is no body to begin with...

If I send the following request curl -H "Content-Type: application/json" -X POST -d '{"command": "Hello"}' http://192.168.0.22/ to the ESP, which looks like this:

POST / HTTP/1.1
User-Agent: curl/7.37.0
Host: 127.0.0.1
Accept: */*
Content-Length: 20

{"command": "Hello"}

then the sketch returns No body?!:

#include <ESP8266WiFi.h>

#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncWiFiManager.h> // https://github.com/alanswx/ESPAsyncWiFiManager/

#include "AsyncJson.h"
#include "ArduinoJson.h"

AsyncWebServer server(80);
DNSServer dns;

void setup() {

  Serial.begin(115200);

  AsyncWiFiManager wifiManager(&server, &dns);
  wifiManager.autoConnect("AutoConnectAP");

  server.on("/", HTTP_POST, [](AsyncWebServerRequest * request) {
    // Count number of params
    int params = request->params(); // 0
    Serial.println(params);
    if (request->hasParam("body", true)) { // This is important, otherwise the sketch will crash if there is no body
      request->send(200, "text/plain", request->getParam("body", true)->value());
    } else {
      Serial.println("No body?!");
      request->send(200, "text/plain", "No body?!\n");
    }
  });

  server.begin();
}

void loop() {
}
probonopd commented 7 years ago

It seems like others were having similar issues too but this never was clearly documented: https://github.com/me-no-dev/ESPAsyncWebServer/issues/123

stelgenhof commented 7 years ago

@probonopd I can retrieve the JSON body if I use it this way: ->value().c_str(). But probably it is better to use the onRequestBody callback.

probonopd commented 7 years ago

That segfaults for me:


#include <ESP8266WiFi.h>

#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncWiFiManager.h> // https://github.com/alanswx/ESPAsyncWiFiManager/

#include "AsyncJson.h"
#include "ArduinoJson.h"

AsyncWebServer server(80);
DNSServer dns;

void setup() {

  Serial.begin(115200);

  AsyncWiFiManager wifiManager(&server, &dns);
  wifiManager.autoConnect("AutoConnectAP");

  server.on("/", HTTP_POST, [](AsyncWebServerRequest * request) {
      request->send(200, "text/plain", request->getParam("body", true)->value().c_str());
  });

  server.begin();
}

void loop() {
}

But probably it is better to use the onRequestBody callback.

How would we do that?

probonopd commented 7 years ago

This seems to be working, thanks @sochs for the hint:

#include <ESP8266WiFi.h>

#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncWiFiManager.h> // https://github.com/alanswx/ESPAsyncWiFiManager/

#include "AsyncJson.h"
#include "ArduinoJson.h"

AsyncWebServer server(80);
DNSServer dns;

void setup() {

  Serial.begin(115200);

  AsyncWiFiManager wifiManager(&server, &dns);
  wifiManager.autoConnect("AutoConnectAP");

  server.onRequestBody([](AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
    Serial.println("Running");
    if (request->url() == "/test") {
      DynamicJsonBuffer jsonBuffer;
      JsonObject& root = jsonBuffer.parseObject((const char*)data);
      if (root.success()) {
        if (root.containsKey("command")) {
          Serial.println(root["command"].asString()); // Hello
        }
      }
      request->send(200, "text/plain", "end");
    }
  });
  server.begin();
}

void loop() {
}
stelgenhof commented 7 years ago

Going to try that! Thanks @probonopd

stelgenhof commented 7 years ago

@probonopd It works great! Thanks for the example.

devyte commented 7 years ago

"Use the source Luke!"

I'm leaving this here in case anyone else is looking for this answer.

Using the onRequestBody() callback doesn't work for me, and even if it did, it is a single callback for all endpoints. the body param also didn't work for me, the received request never received a body parameter.

After looking through the source, I found that there are 3 versions of the server.on() method for setting callbacks. The first is the simple one, as seen in most examples: server.on(endpoint, HTTP_POST, callback) The second is a flavor used for file upload. It takes two callbacks, the second of which handles file chunks: server.on(endpoint, HTTP_POST, callback, onFileUploadCB) And drum roll... the third flavor takes 3 callbacks: server.on(endpoint, HTTP_POST, callback, onFileUploadCB, onBodyCB) The first two are same as in the previous case, and the third is an onBody handler, called when the body is received. This can show how these work:

  server.on("/test", HTTP_POST, 
      [](AsyncWebServerRequest *request)
      {
        Serial.println("1");
      },
      [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final)
      {
        Serial.println("2");
      },
      [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
      {
        Serial.println("3");
        Serial.println(String("data=") + (char*)data);
      });

From what I gather, when posting with a body, the 2nd callback isn't used. In theory, the 3rd callback can be called several times, because the body can be received in chunks. For me, it gets called only once for bodies up to about 1.5KB. Didn't test with bigger bodies.

This doesn't seem documented, but it works for me, and the semantics look correct. Anyways, I hope this helps somebody.

klaasdc commented 6 years ago

I tried the code of devyte with a jquery.post, but the third handler is never called for me. I don't understand why, it just gets skipped.

The code of jeremypoulter using the single request handler but checking for body data does work. It feels a bit like a hack to not use the intended onBodyHandler for though...

dphans commented 6 years ago

@probonopd Your example is nice (server.onRequestBody...). But I'm worry using server.onRequestBody instead of server.on... Any requests will come to server.onRequestBody first? then going to server.on. What happen if we response to user via request->send before another routes handle?

For simple example:

_asyncWebServer.onRequestBody([this] (AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
    if (request->url() == "/api" && request->method() == HTTP_POST) {
      const char* requestBody = (const char*) data;
      this->debug("[POST /api]: %s", requestBody);
    }
    request->send(200, "application/json", "{}");
  });
_asyncWebServer.on("/", [this] (AsyncWebServerRequest * request) {
  request->send(200, "text/html", "<body>Example</body>");
});

So, if user enter /. Is it reponse html contents from server.on or json contents from server.onRequestBody?

maxdd commented 6 years ago

A more elegant way that works for me

server.on("/generate", HTTP_POST, [](AsyncWebServerRequest *request){
    //nothing and dont remove it
  }, NULL, [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){
    DynamicJsonBuffer jsonBuffer;
    JsonObject& root = jsonBuffer.parseObject((const char*)data);
    if (root.success()) {
      if (root.containsKey("cmd")) {
        Serial.println(root["cmd"].asString());
      }
      if (root.containsKey("cmd1")) {
        Serial.println(root["cmd1"].asString());
      }
      request->send(200, "text/plain", "");
    } else {
      request->send(404, "text/plain", "");
    }
  });
Menion2k commented 5 years ago

Hello I am trying to do the same on my project. I tried to add a callback in server.on, as done by @maxdd but then I hit the issue #287 where body callback is called multiple time, causing Json parsing to fail. So I tried the new AsuncCallbaclJsonWebHandler method doing:

  AsyncCallbackJsonWebHandler* statusJson = new AsyncCallbackJsonWebHandler("/rest/status.json", statusPostJson);
server.addHandler(statusJson);

then in the html page I send a POST request with json body with jquery ajax request as:

function sendJSON() {
        $.ajax({
            url: 'rest/status.json',
            type: 'POST',
            data: JSON.stringify(Status),
            contentType: 'application/json; charset=utf-8',
            dataType: 'json',
            async: false,
            success: function(msg) {
                alert(msg);
            }
        });
    } 

The result is that the callback statusPostJson is never called. The ajax method does work, since I used with ESP8266WebServer and it is capable to push the Json in body since I receive it with the .on callback method. Any tips? There must be something I miss or doing wrong, but I cannot figure it out

ammm56 commented 5 years ago

I tried the code of @devyte and @probonopd .

server.on("/api/tokenverify",HTTP_POST,[](AsyncWebServerRequest request){ Serial.println("END"); },NULL,[](AsyncWebServerRequest request,uint8_t data,size_t len,size_t index,size_t total){ Serial.println("IN /api/tokenverify"); Serial.printf("[len]: %d [index]: %d [total]: %d \n",len,index,total); String strdata = (char)data; Serial.println(String("[data]:")+strdata); request->send(200, "text/plain", "END"); });

server.onRequestBody([](AsyncWebServerRequest request, uint8_t data, size_t len, size_t index, size_t total){ if (request->url() == "/test") { Serial.println("IN /test"); String strdata = (char*)data; Serial.println(String("[data]:")+strdata); } request->send(200, "text/plain", "END"); });

Post body data received a chunked response,One request was split twice.Results as:

IN /test

IN /test

IN /api/tokenverify

IN /api/tokenverify

END

The correct result is:

IN /test

Where is the problem?

stale[bot] commented 5 years ago

[STALE_SET] This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions.

stale[bot] commented 5 years ago

[STALE_CLR] This issue has been removed from the stale queue. Please ensure activity to keep it openin the future.

pcbtborges commented 5 years ago

Hi, what libraries are required to use server.arg? All I have is WiFiServer server(80); and it says: 'class WiFiServer' has no member named 'arg'

I am getting all the data I need (SEE BOLD TEXT) when my ESP32webServer gets JSON from a web browser but, so far, I am unable to get to the JSON data on the ESP32 side.

My Problem is: Get to the JSON data arriving from the browser.

What is displayed at the ESP32 console is:

`New Client. GET /?wifissid=&wifiPass=&delayPrimFoto=&delayEntreFotos=&timeGMT=2&smtpServer=&smtpUser=&smtpPass=&emailto=&emailTitle=&emailMessage=&ftpServer=&ftpUser=&ftpPass= HTTP/1.1 Host: 192.168.4.1 Connection: keep-alive Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Linux; Android 9; Mi A2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36 EdgA/42.0.4.3928 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8 Referer: http://192.168.4.1/ Accept-Encoding: gzip, deflate Accept-Language: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7

Client disconnected.

New Client. ` Assistance welcome Paulo

mrWheel commented 4 years ago

Hi, Given this code:

  httpServer.on("/api/v0/get", HTTP_GET, 
        [](AsyncWebServerRequest *request) 
        {
            v0_Get_restAPI_1(request);
        }, 
        NULL, //-- leave NULL in or it won't compile ....
        [](AsyncWebServerRequest *request, uint8_t *bodyData, size_t bodyLen, size_t index, size_t total)
        {
            v0_Get_restAPI_3(request, bodyData, bodyLen);
        }
  );

for every request with params at the url like /api/v0/get/order?orderid="1234" The function "v0_Get_restAPI_1(request)" is called and processed.

But if I call curl -XGET --data '{"orderid" : "1234"}' http://server/api/v0/get/order

first v0_Get_restAPI_3(request, bodyData, bodyLen) is executed and right after that v0_Get_restAPI_1(request) is executed!

How can I prevent executing v0_Get_restAPI_1(request) if v0_Get_restAPI_3(request, bodyData, bodyLen) is already executed??

sparkplug23 commented 4 years ago

Did anyone here find a better solution or has a working solution they could share a little example of. I have spent 3 days and can't read the body of the message no matter what I have tried (all of the above too). Many thanks.

BlueAndi commented 4 years ago

@sparkplug23 Could be the problem how you send the JSON data?

I tested the following and it works for me:

    srv.onRequestBody(
        [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
        {
            if ((request->url() == "/rest/api/v2/test") &&
                (request->method() == HTTP_POST))
            {
                const size_t        JSON_DOC_SIZE   = 512U;
                DynamicJsonDocument jsonDoc(JSON_DOC_SIZE);

                if (DeserializationError::Ok == deserializeJson(jsonDoc, (const char*)data))
                {
                    JsonObject obj = jsonDoc.as<JsonObject>();

                    LOG_INFO("%s", obj["test"].as<String>().c_str());
                }

                request->send(200, "application/json", "{ \"status\": 0 }");
            }
        }
    );

POST JSON via curl:

$ curl -H "Content-Type: application/json" --data '{ "test": "Hello!" }' -X POST http://192.168.2.91/rest/api/v2/test
sparkplug23 commented 4 years ago

@BlueAndi

Thank you so much! What my exact problem was, I have no clue. I was using almost exactly what you shared with me there, but for some reason it was not working. I tested your code with a clean install of ArduinoJson and Asyncwebserver in arduino ide (I use vscode normally) and managed to get your example working eventually.

I was using "Postman" and after comparing your curl example and mine, postman does in fact work, but I may have had it on the wrong thing. You know yourself, you change things on both ends and probably miss the fix that resolved the error due to some debug edit.

Anyway, sincere thanks to helping me resolve a 4 day code headache. Now to actually build the code up.

lkwilson commented 3 years ago

AsyncJson.h has a good example of a JSON request and response. Here's what I've adapted from it:

#include <ESPAsyncWebServer.h>
#include <AsyncJson.h>
#include <ArduinoJson.h>
...
server.addHandler(new AsyncCallbackJsonWebHandler(
  "/api/endpoint",
  [this](AsyncWebServerRequest* request, JsonVariant& json) {
      if (not json.is<JsonObject>()) {
        request->send(400, "text/plain", "Not an object");
        return;
      }
      auto&& data = json.as<JsonObject>();

      if (not data["name"].is<String>()) {
        request->send(400, "text/plain", "name is not a string");
        return;
      }
      String name = data["name"].as<String>();

      if (name == "IDLE") {
        set_mode_idle(request, data); // handle data and respond
      } else if (name == "RANDOM") {
        set_mode_random(request, data); // handle data and respond
      } else {
       request->send(400, "text/plain", "Invalid mode");
      }
}));
...
dduehren commented 2 years ago

Here's what I did, example of xhttp and JSON.

function parseResponse(p1){ jsonResponse = p1; document.getElementById("temperature").innerHTML = jsonResponse.TMP; document.getElementById("humidity").innerHTML = jsonResponse.HUM; document.getElementById("CO2Lvl").innerHTML = jsonResponse.CO2; document.getElementById("light").innerHTML = jsonResponse.LED; document.getElementById("time").innerHTML = jsonResponse.TIM; document.getElementById("dCount").innerHTML = jsonResponse.CNT; document.getElementById("error").innerHTML = jsonResponse.ERR; document.getElementById("wifi").innerHTML = jsonResponse.WIFI; } setInterval(function ( ) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200){ var jsonResponse = JSON.parse(this.responseText); console.log(jsonResponse); parseResponse(jsonResponse); } }; xhttp.open("GET", "/Main", true); xhttp.send(); }, 2000 ) ;

The server.on route:

server.on("/Main", HTTP_GET, [](AsyncWebServerRequest *request){ webProc.mainUpdateJson(); request->send_P(200, "text/plain", MTemp);
});

And the main.UpdateJson function:

void WebProc::mainUpdateJson(){ StaticJsonDocument<200> doc; //document has to be bigger than the space required for the output json text. if(tempMode){ fltvar = (sense.getTemp()*9.0/5.0)+32; snprintf(MTemp, 10, "%3.1f %c%cF", fltvar, 0xC2, 0xB0); MTemp, sense.getTemp()); } else { snprintf(MTemp, 10, "%3.1f %c%cC",sense.getTemp(), 0xC2, 0xB0); } doc["TMP"] = MTemp; snprintf(MTemp, 10, "%2.2f%s",sense.getHumidity()," %"); doc["HUM"] = MTemp; snprintf(MTemp, 10, "%6.1f",sense.getCO2()); doc["CO2"] = MTemp; tempbool = leds.getLedState(); if(tempbool){ doc["LED"] = FPSTR(string_On); } else { doc ["LED"]= FPSTR(string_Off); } doc["TIM"] = timeClient.getFormattedTime(); snprintf(MTemp, 5, "%d", shroom.getDayCount()); doc["CNT"] = MTemp; snprintf(MTemp, 3, "%d", leds.getLedPattern()); doc["ERR"] = MTemp; intvar = wifiMgr.getWifiStatus(); switch(intvar){ case WiFiMgr::AP_STAconn: doc["WIFI"]= FPSTR(string_AP_STA); break; case WiFiMgr::APconn: doc["WIFI"] = FPSTR(string_APMode); break; case WiFiMgr::STAconn: doc["WIFI"]= FPSTR(string_CONN); break; case WiFiMgr::DISconn: doc["WIFI"]= FPSTR(string_DISCONN); break; default: WiFiMgr::AP_STAconn); doc["WIFI"]= FPSTR( string_NotConfigured); break; } serializeJson(doc, MTemp); }`

postyu commented 2 years ago

Hi, Given this code:

httpServer.on("/api/v0/get", HTTP_GET, [](AsyncWebServerRequest request) { v0_Get_restAPI_1(request); }, NULL, //-- leave NULL in or it won't compile .... [](AsyncWebServerRequest request, uint8_t *bodyData, size_t bodyLen, size_t index, size_t total) { v0_Get_restAPI_3(request, bodyData, bodyLen); } ); for every request with params at the url like /api/v0/get/order?orderid="1234" The function "v0_Get_restAPI_1(request)" is called and processed.

But if I call curl -XGET --data '{"orderid" : "1234"}' http://server/api/v0/get/order

first v0_Get_restAPI_3(request, bodyData, bodyLen) is executed and right after that v0_Get_restAPI_1(request) is executed!

How can I prevent executing v0_Get_restAPI_1(request) if v0_Get_restAPI_3(request, bodyData, bodyLen) is already executed??

Hi mrWheel,

Sorry if my answer comes so late, but you need to use solution like this (copied from the AsyncJson.h):

httpServer.on("/api/v0/get", HTTP_GET, [](AsyncWebServerRequest request) { DynamicJsonBuffer jsonBuffer; JsonVariant json = jsonBuffer.parse((uint8_t)(request->_tempObject)); // tempObject is a pointer, so you can also boxing it like any other type [ your own code here]_

     }, NULL,
     [](AsyncWebServerRequest *request, uint8_t *bodyData, size_t bodyLen, size_t index, size_t total)
     {
    if (total > 0 && request->_tempObject == NULL && total < 10240) { // you may use your own size instead of 10240
        request->_tempObject = malloc(total);
    }
    if (request->_tempObject != NULL) {
        memcpy((uint8_t*)(request->_tempObject) + index, bodyData, bodyLen);
    }
     });

As described in readme.md: _If needed, the _tempObject field on the request can be used to store a pointer to temporary data (e.g. from the body) associated with the request. If assigned, the pointer will automatically be freed along with the request._