tokio-rs/axum

Non-cancellable handlers

Closed this issue · 6 comments

  • I have looked for existing issues and discussions (including closed) and the ecosystem about this

Feature Request

Motivation

When a request is being handled but the connection is dropped either because the client lost interest, because of network issue or any other reason, the future with the handler is cancelled and dropped.

This can often not be what the server should do.

Proposal

Adding an optional layer that would spawn the handler as a task, letting tokio run it to completion if the request is aborted midway through.

Alternatives

We could instead create a layer that does some check of the state or cleanup in case of a drop. This would probably be slightly more complicated by the fact that we do not have async drop so we probably could use channels for notifications and run it elsewhere.

There is also the option of not having this in axum (or rather axum-extra) at all, either putting it in another crate or letting the users just spawn tasks in their handlers when they need to.

Hm, I don't think there is anything axum- or even http-specific about this so IMO this should be a middleware in tower[-util].

Yeah it would basically be a middleware that tokio::spawns the handler. Could have in tower-http.

Thanks, I'll try to offer it there.

Hi, Sorry for bother. I've come across a situation similar to the one discussed here, and I'm seeking some guidance. I want to do some extra cleanup work whenever client abort its request. I was wondering if you could share any advice or point me towards resources that might help with managing this scenario using tower-http.
Thank you very much for your time and help !

If you don't need to use async for the cleanup, you can create a guard that will do cleanup when dropped. When the future is dropped, your guard will be dropped as well and its Drop implementation will be run. But try not to block there as it's still in an async context.

If you need async cleanup, it might be a bit more involved, I guess the simplest thing would be to use the same thing but using tokio::spawn to do the cleanup, if the Send and 'static bounds permit it.

Yeah, I have thought to use RAII. But in my specific scenario, I build a sse event stream response, and I want to perform the cleanup task after the stream response completes or if the client aborts its connection, if I initialize the guard just in the Axum handler and do not do any other things, its Drop will be invoked before the event stream iterates, this is not what I required. However, I found a method that satisfy my need. I try to use a move closure to capture the guard object within the stream, this extends the guard's lifetime so that it will be dropped after the event stream completes.

Here's my simplified version of the code:

struct Guard;
impl Guard {
    fn noop(&self) {}
}
impl Drop for Guard {
    fn drop(&mut self) {
        tracing::info!("a `Guard` was dropped!")
    }
}

async fn handler() -> Response {
    let guard = Guard;
    // ... some handler logic here
    
    let sse_event = futures::StreamExt::enumerate(stream).map(move |(id, chunk)| {
        guard.noop();
        match chunk {
            Ok(data) => Event::default().json_data(data),
            Err(e) => Event::default().json_data(e.to_string()),
        }
    }); 
    let sse = Sse::new(sse_event).keep_alive(
                    axum::response::sse::KeepAlive::new()
                        .interval(Duration::from_secs(1))
                        .text("keep-alive-text"),
                );
    sse.into_response()
}