actix-web icon indicating copy to clipboard operation
actix-web copied to clipboard

Improvements on streaming()

Open dynos01 opened this issue 2 years ago • 0 comments
trafficstars

PR Type

Bug Fix

PR Checklist

  • [ ] Tests for the changes have been added / updated.
  • [x] Documentation comments have been added / updated.
  • [x] A changelog entry has been made for the appropriate packages.
  • [x] Format code with the latest stable rustfmt.
  • [x] (Team) Label with affected crates and semver status.

Overview

This PR changes the behavior of actix_web::response::builder::HttpResponseBuilder::streaming().

Before this PR, if Content-Type is not set by the user, then there is no such header in the final response. This PR adds a default Content-Type of application/octet-stream, just like actix_web::response::builder::HttpResponseBuilder::json() defaults the Content-Type to application/json.

Before this PR, if the user does not call actix_web::response::builder::HttpResponseBuilder::no_chunking() but sets Content-Length, then the header is silently dropped, as described in #2306. This PR respects the user's input: if the user sets Content-Length, then actix_web::response::builder::HttpResponseBuilder::no_chunking() is automatically called with the corresponding stream length.

A small illustration program for the change:

use std::{
    pin::Pin,
    task::{Context, Poll},
};

use actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, Responder, http::header};
use futures::stream::Stream;

struct ZeroStream {
    n: u64,
}

impl Stream for ZeroStream {
    type Item = Result<web::Bytes, actix_web::Error>;

    fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
        let resp = vec![0u8; self.n as usize];
        Poll::Ready(Some(Ok(web::Bytes::copy_from_slice(&resp))))
    }
}

#[get("/{n}")]
async fn zero(req: HttpRequest) -> impl Responder {
    let n: &str = match req.match_info().get("n") {
        Some(n) => n,
        None => return HttpResponse::BadRequest().finish(),
    };
    let n: u64 = match n.parse() {
        Ok(n) => n,
        Err(_) => return HttpResponse::BadRequest().finish(),
    };

    let stream = ZeroStream {
        n,
    };

    HttpResponse::Ok()
        .append_header((header::CONTENT_LENGTH, format!("{n}")))
        .streaming(stream)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(zero)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Request:

GET /100 HTTP/1.1

The response is now:

< HTTP/1.1 200 OK
< content-length: 100
< content-type: application/octet-stream
< date: Fri, 14 Apr 2023 15:49:33 GMT

instead of:

< HTTP/1.1 200 OK
< transfer-encoding: chunked
< date: Fri, 14 Apr 2023 16:19:26 GMT
// Downloading endlessly

Closes #2306

dynos01 avatar Apr 14 '23 16:04 dynos01