Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

middleware::Compress seems to be buffering content indefinitely #3410

Open
lovasoa opened this issue Jun 23, 2024 · 1 comment
Open

middleware::Compress seems to be buffering content indefinitely #3410

lovasoa opened this issue Jun 23, 2024 · 1 comment
Labels
A-web project: actix-web C-bug Category: bug

Comments

@lovasoa
Copy link

lovasoa commented Jun 23, 2024

Initially reported sqlpage/SQLPage#435

When making streaming responses and activating middleware::Compress, the compression seems to buffer content indefinitely, without ever flushing the streaming data when it's small enough.

Here is an example demonstrating the problem

use actix_web::{
    get, middleware,
    web::{self, Bytes},
    App, HttpResponse, HttpServer,
};
use futures::stream::{self, StreamExt};
use std::time::Duration;
use tokio::time::sleep;

#[get("/")]
async fn index() -> HttpResponse {
    let stream = stream::once(async {
        let initial_content = r#"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Streaming HTML Example</title>
</head>
<body>
    <div>This content appears immediately</div>
"#;
        Ok::<_, std::io::Error>(Bytes::from(initial_content))
    })
    .chain(stream::once(async {
        sleep(Duration::from_secs(5)).await;
        let delayed_content = r#"
    <div>This content appears after 5 seconds</div>
</body>
</html>
"#;
        Ok::<_, std::io::Error>(Bytes::from(delayed_content))
    }));

    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .streaming(stream)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/compressed")
                    .wrap(middleware::Compress::default())
                    .service(index),
            )
            .service(index)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Expected Behavior

  • When visiting I should see This content appears immediately immediateley, and then after 5 seconds This content appears after 5 seconds
  • When visiting http://localhost:8080/compressed/ the behavior should be the same, or as close as possible.

Current Behavior

  • When visiting http://localhost:8080/compressed/ the page stays blank for five seconds, then displays This content appears immediately and This content appears after 5 seconds at the same time.

Possible Solution

The compression middleware should flush the writer object after a few milliseconds without new input data, to avoid indefinitely retaining data in memory on the server when it could actually already be rendered on the client.

It's possible to retain data in memory for a few milliseconds to ensure good compression ratios, but actix shouldn't keep streaming data in memory for multiple seconds.

Context

We are trying to integrate easy native loading spinners without javascript in SQLPage

Your Environment

  • Actix Web Version: 4.8.0
@kevincox
Copy link

I would also like to see support for chunked responses with compression.

Problem

I think this may be an API limitation. The actix_web::body::BodyStream type is a stream of actix_web::web::Bytes objects. There is no side channel to express the desire to flush the stream.

With non-compressed streams it works because each Bytes object is assumed to be a chunk of data that should be flushed eagerly. However with compression it isn't so simple. If a chunk compresses very well it may be desirable to not flush a tiny buffer and send a small packet over the wire. However this is clearly suboptimal in cases like this where there is no next chunk soon to be available.

It would be nice to be able to do both:

  1. If the next chunk is not ready flush the current chunk.
  2. Provide an API for explicit flushing.

However this may require a pretty substantial change to how Body looks.

Workaround

For my app only a small number of endpoints rely on streaming chunked responses so I disabled compression on these routes by stripping the Accept-Encoding header. Ugly, but it lets me get the benefits of compression on most of my routes without throwing away streaming responses.

actix_web::App::new()
	.wrap(actix_web::middleware::Compress::default())
	.wrap_fn(|mut req, srv| {
		if req.path() == "/subscriptions/import" {
			req.headers_mut().remove(actix_web::http::header::ACCEPT_ENCODING);
		}

		actix_web::dev::Service::call(srv, req)
	})
	.default_service(...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-web project: actix-web C-bug Category: bug
Projects
None yet
Development

No branches or pull requests

3 participants