PsychicHttp icon indicating copy to clipboard operation
PsychicHttp copied to clipboard

[Question] Request disconnect callback

Open zekageri opened this issue 2 years ago • 24 comments

In AsyncWebserver we can attach a callback to the request and it gets called after disconnect. This is usefull because if i want to allocate some memory in the request handler i can free it in the disconnect callback

if( request->url() == "/auth/getCurrentUser" ){
    char* userJsonString;
    if( !alloc(userJsonString,5000) ){ // Allocate a buffer for the json string.
        return request->reply(400);
    }
    request->onDisconnect([userJsonString](){
        free(userJsonString); // Free on disconnection so we don't have to deal with it.
    });
    String sessionID;
    if( !request->hasHeader("Cookie") ){
        request->send(400);
        return;
    }
    AsyncWebHeader *allCookie = request->getHeader("Cookie");
    hshServer.getCookie("sessionID", allCookie->value(), sessionID);
    // Rest of the code...
}

Sorry this is half async webserver and half PsychicHttp right now :D

zekageri avatar Dec 11 '23 08:12 zekageri

More real world example

Instead of this

if( request->url() == "/auth/getCurrentUser" ){
    char* userJsonString;
    if( !hshSystem.alloc(userJsonString,USERS_JSON_SIZE) ){
        return request->reply(400);
    }
    if( !request->hasSessionKey("sessionID") ){
        free(userJsonString); // Free
        return request->reply(400);
    }
    String sessionID = request->getSessionKey("sessionID");
    DynamicJsonDocument userDoc(USERS_JSON_SIZE);
    JsonObject userObj = userDoc.to<JsonObject>();
    if( !user.getUserBySession(sessionID.c_str(),userObj) ){
        free(userJsonString); // Every
        return request->reply(400);
    }
    if( serializeJson(userObj,userJsonString,USERS_JSON_SIZE) < 1 ){
        free(userJsonString); // single
        return request->reply(400);
    }
    esp_err_t result = request->reply(200,"application/json",userJsonString);
    free(userJsonString); // time
    return result;
}

It would became this

if( request->url() == "/auth/getCurrentUser" ){
    char* userJsonString;
    if( !hshSystem.alloc(userJsonString,USERS_JSON_SIZE) ){
        return request->reply(400);
    }
    request->onDisconnect([userJsonString](){
        free(userJsonString); // Free after disconnection
    });
    if( !request->hasSessionKey("sessionID") ){
        return request->reply(400);
    }
    String sessionID = request->getSessionKey("sessionID");
    DynamicJsonDocument userDoc(USERS_JSON_SIZE);
    JsonObject userObj = userDoc.to<JsonObject>();
    if( !user.getUserBySession(sessionID.c_str(),userObj) ){
        return request->reply(400);
    }
    if( serializeJson(userObj,userJsonString,USERS_JSON_SIZE) < 1 ){
        return request->reply(400);
    }
    return request->reply(200,"application/json",userJsonString);
}

zekageri avatar Dec 11 '23 08:12 zekageri

I can definitely add this. I had support for it partially added, but wasn't sure that it was useful or not. I will re-add that back in and let you know.

hoeken avatar Dec 12 '23 18:12 hoeken

I'm looking at implementing this now, but the way its done with websockets and eventsource, the client list is kept in the handler object itself. If I was to implement it for any generic handler, then you'd end up with new sessions getting started in each handler.

Have you seen the server.onOpen and onClose callbacks? If you're doing sessions, that would probably be the place to put it.

hoeken avatar Dec 13 '23 15:12 hoeken

Actually i dont use sessions. I have an spa. I wanted to use it for user login time but i get back to using cookies.

zekageri avatar Dec 13 '23 17:12 zekageri

But i see what you mean

zekageri avatar Dec 13 '23 17:12 zekageri

I have a few old Async* request->onDisconnect()'s I like to port over. Is there a standard way of doing this now (not ES or WS).

proddy avatar Dec 19 '23 20:12 proddy

Couldn't sleep, added in the callbacks:

//client connect/disconnect to a url
// curl -i http://psychic.local/handler
PsychicWebHandler *connectionHandler = new PsychicWebHandler();
connectionHandler->onRequest([](PsychicRequest *request)
{
  return request->reply("OK");
});
connectionHandler->onOpen([](PsychicClient *client) {
  Serial.printf("[handler] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString());
});
connectionHandler->onClose([](PsychicClient *client) {
  Serial.printf("[handler] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
});

hoeken avatar Dec 22 '23 05:12 hoeken

Its worth noting that it will only track clients to that specific url, so if you need to do something globally, use the server.onOpen and server.onClose instead

hoeken avatar Dec 22 '23 05:12 hoeken

Really nice! Maybe this will let us defer the requests too? :O

zekageri avatar Dec 22 '23 07:12 zekageri

Oh this is not what i was expecting. I expected the disconnect callback on the request object

server.on("/test", HTTP_GET, [](PsychicRequest *request){
    request->onDisconnect([](){
        // Handle something on request disconnection.
    });
    // Handle the request...
});

zekageri avatar Dec 22 '23 21:12 zekageri

@zekageri unfortunately getting a disconnect event on requests is unlikely to happen for a couple reasons:

  • ESP-IDF only provides a connection open/close event at the server level, there is really no way to associate it with a request. best we can do is track what clients belong to what handlers and pass the event to the handler
  • a request doesnt always end with a disconnection. most modern browsers will keep the connection open for further requests.

hoeken avatar Dec 22 '23 23:12 hoeken

That actually makes a lot of sense

proddy avatar Dec 22 '23 23:12 proddy

Oh. Excuse my dumbness but i dont understand how should i add the handler to a url. If I manage to add this dc handler to a specific url somehow isnt it the same?

zekageri avatar Dec 23 '23 07:12 zekageri

Doesnt the server free the socket on response? If I allocate some memory in the handler, when should i free it if the socket never freed?

zekageri avatar Dec 23 '23 08:12 zekageri

Oh. Excuse my dumbness but i dont understand how should i add the handler to a url. If I manage to add this dc handler to a specific url somehow isnt it the same?

The basic flow goes like this:

server.onOpen(client) handler.onOpen(client) handler.onRequest(request) //potentially multiple requests from client handler.onClose(client) server.onClose(client)

If you need to allocate memory for a user session you can do it in the onOpen and free it in the onClose. Most likely you want it on a per-server basis, but there is the options for a per-handler basis too.

hoeken avatar Dec 23 '23 14:12 hoeken

Doesnt the server free the socket on response? If I allocate some memory in the handler, when should i free it if the socket never freed?

No, the server frees all the socket memory when the connection is closed, either normally or if there is an error. That connection can stay open for multiple requests. I would avoid allocating memory in the onRequest callback unless you are freeing it within that same function. If you really need to, use the onOpen() and onClose()

hoeken avatar Dec 23 '23 14:12 hoeken

@hoeken I have a similar use case to @zekageri 's.

I have a 'Restart' button on a web page that reboots the ESP32 but I want to gracefully exit from the response first before the backend code calls ESP.restart();.

In AsyncWS this could be done using request->onDisconnect() as a post-action, to make the restart call.

Can you think of nifty way to do something similar with PsychicHttp?

proddy avatar Dec 23 '23 17:12 proddy

My problem is the following. In the request cb i allocate memory in ext ram. I need a huge chunk. I create a json document and fill it with data. I serialize the doc to the allocated char array and let the server send it. Since it is a large array the server will chunk it and send it chunked. Since i cant wait for it in the cb because i have to return immediately i can't free the memory while its sending. So I need a dc cb to be able to free the memory when this large array sent.

zekageri avatar Dec 23 '23 17:12 zekageri

At least that was the case with the previous lib. I also did a lot of deferred request and used disconnect callbacks because i had huge responses

zekageri avatar Dec 23 '23 17:12 zekageri

Huge for the esp at least

zekageri avatar Dec 23 '23 17:12 zekageri

@proddy @zekageri I hear what you guys are saying and am trying to figure out how to make it work. There are a couple things at play here, the main one being that we only really have 3 points of contact with the server: the onOpen, onRequest, and onClose callback stuff provided by ESP-IDF http server.

@proddy have you tried calling the request->reply() or response->send(), storing the return value, then doing a reboot? the response->send() function directly sends the data over tcp, so it should be okay for the server to reboot after that. you might even be able to do something like response->client()->close() to make it explicit, or even loop over all the server clients and call close on each one. The return from the onRequest function is mostly just to let the ESP-HTTP server know if handling the request was successful or not (and closes the connection on anything other than ESP_OK).

@zekageri - i still don't have a good answer for how to handle your deferred responses. i should really modify the main page and add an asterisk to the multithreading part. The whole server runs in its own thread, but processes requests one at a time. in 5.1.2 there is support for true multithreaded processing where each request gets handled in its own thread. that will let you just do your long request in one callback. I tried backporting the changes from 5.1.2, but I couldn't get it to work. You can see my attempt in the async_worker.h and async_worker.cpp files. You can enable it by defining ENABLE_ASYNC. I'm a bit out of my depth with the underlying freertos threading stuff so maybe i missed something in the backport.

hoeken avatar Dec 23 '23 21:12 hoeken

thanks @hoeken - it was a problem with my code in the end (always is!). I had

 request->reply(200);
 WiFi.disconnect(true);    <----- removed this line
 delay(500);
 ESP.restart();

The wifi disconnect was too quick and killed the connection before the httpd reply could be sent. Removing the Wifi.disconnect fixed it.

proddy avatar Dec 24 '23 12:12 proddy

So this type of dc cb will never happen?

server.on("/test", HTTP_GET, [](PsychicRequest *request){
    request->onDisconnect([](){
        // Handle something on request disconnection.
    });
    // Handle the request...
})

zekageri avatar Jan 04 '24 14:01 zekageri

It gets called when that client closes it's connection to the server. You can test it with curl which should open and close it all in one go, but if you do it with a web browser they tend to keep connections open for a while in case there are more requests so it is delayed.

On Thu, Jan 4, 2024, 09:15 DrRandom @.***> wrote:

So this type of dc cb will never happen?

server.on("/test", HTTP_GET, [](PsychicRequest *request){ request->onDisconnect({ // Handle something on request disconnection. }); // Handle the request... })

— Reply to this email directly, view it on GitHub https://github.com/hoeken/PsychicHttp/issues/12#issuecomment-1877162235, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABEHSQ6YEUTLPEFAG5RRDTYM22Q7AVCNFSM6AAAAABAPKPBZSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNZXGE3DEMRTGU . You are receiving this because you were mentioned.Message ID: @.***>

hoeken avatar Jan 04 '24 17:01 hoeken

I'm closing all these old issues. Please re-open it if needed.

hoeken avatar Aug 10 '24 18:08 hoeken