5

I am attempting to use actix_web to fetch and display the contents of a web page. The HTTP request completes successfully and I can view the webpage, but I want to read the body into a String for printing.

I tried let my_ip: String = response.body().into(); but I get an error that says

error[E0277]: the trait bound `std::string::String: std::convert::From<actix_web::httpmessage::MessageBody<actix_web::client::response::ClientResponse>>` is not satisfied
  --> src/main.rs:16:53
   |
16 |                 let my_ip: String = response.body().into();
   |                                                     ^^^^ the trait `std::convert::From<actix_web::httpmessage::MessageBody<actix_web::client::response::ClientResponse>>` is not implemented for `std::string::String`
   |
   = help: the following implementations were found:
             <std::string::String as std::convert::From<&'a str>>
             <std::string::String as std::convert::From<std::borrow::Cow<'a, str>>>
             <std::string::String as std::convert::From<std::boxed::Box<str>>>
             <std::string::String as std::convert::From<trust_dns_proto::error::ProtoError>>
   = note: required because of the requirements on the impl of `std::convert::Into<std::string::String>` for `actix_web::httpmessage::MessageBody<actix_web::client::response::ClientResponse>`

This is what I have so far:

use actix;
use actix_web::{client, HttpMessage};
use futures::future::Future;

fn main() {
    actix::run(|| {
        client::get("http://ipv4.canhasip.com/")
            .header("User-Agent", "Actix-web")
            .finish()
            .unwrap()
            .send()
            .map_err(|_| ())
            .and_then(|response| {
                println!("Response: {:?}", response);
                // error occurs here
                let my_ip: String = response.body().into();
                Ok(())
            })
    });
}

Relevant dependency versions:

[dependencies]
actix-web = "0.7"
actix = "0.7"
futures = "0.1"
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
BonsaiOak
  • 27,741
  • 7
  • 30
  • 54
  • 2
    @Shepmaster You're right, I've been on SO long enough to understand that my question left a lot to be desired. I went through and clarified things and made sure that the error message exactly matched the example. – BonsaiOak Jan 08 '19 at 02:14
  • No worries; it's super easy to get caught up in a problem that we've been working on for a while and lose sight of the bigger picture! – Shepmaster Jan 08 '19 at 02:28

3 Answers3

6

In order to keep the response object you have while also extracting the body, we're going to take advantage of the fact that, unlike a few other frameworks, you can extract the body without destructuring the entire object. Code is below:

actix::run(|| {

    client::get("http://localhost/")
        .header("User-Agent", "Actix-web")
        .finish()
        .unwrap()
        .send()
        .map_err(|e| {
          Error::new(ErrorKind::AddrInUse, "Request error", e)
        })
        .and_then(|response| {
          println!("Got response");
          response.body().map(move |body_out| {
            (response, body_out)
          }).map_err(|e| Error::new(ErrorKind::InvalidData, "Payload error", e))
        }).and_then(|(response, body)| {
          println!("Response: {:?}, Body: {:?}", response, body);
          Ok(())
      }).map_err(|_| ())
});

In order:

  • everything in there now uses std::io::Error for ease of use. Since all actix error types implement Error, it is also possible to preserve the original types
  • and_then() allows me to extract the body. When this is resolved, a map (with move making sure we're taking response with us) then returns a tuple of (response, body)
  • From there, you can freely use either the response or the body as you see fit

Do note that I replaced your hostname with localhost for testing purposes as ipv4.canhasip.com does not currently resolve to anything on the outside.


Initial answer:

You really should've provided more context on this one. actix has more than one request type.

Your initial object (response) is a ClientResponse. calling body() on it returns a MessageBody struct, which is the start of the rabbit hole you fell into. This is NOT the actual body, merely an object implementing the Future trait and which will, once it has run its course, yield what you are after.

You'll need to do this in a less hacky way, but for now and to convince yourself that this is the source of the issue, instead of your line of code, try this:

println!("{:?}", response.body().wait())

Calling wait() on a future blocks the current thread, which is why I am saying it is a temporary, hacky way to show you where the issue is. Depending on what you have at your disposal (if you have an executor-like object somewhere, or are able to return a future for execution), your actual solution will differ.

Sébastien Renauld
  • 19,203
  • 2
  • 46
  • 66
  • 1
    Sorry for the lack of detail. I didn't realize there were multiple request types. I've edited the question with an example. See if that helps. – BonsaiOak Jan 08 '19 at 01:21
  • @BonsaiOak reply is delayed, work is being...unusually *interesting*. Editing the answer to provide a bit more. – Sébastien Renauld Jan 09 '19 at 10:31
1

Complementing Sébastien's answer, you can also resolve the MessageBody future:

.and_then(|response| {
    response.body().map_err(|_| ()).and_then(|bytes| {
        println!("{:?}", bytes);
        Ok(())
    })
})
Caio
  • 3,178
  • 6
  • 37
  • 52
  • 1
    I didn't want to go that far as, due to the brevity of the OP's example, it isn't guaranteed that he isn't *already* returning something implementing `IntoFuture` from where he got that object, hence the caveat :-) – Sébastien Renauld Jan 07 '19 at 14:31
  • I've edited my question with an example. See if that helps. – BonsaiOak Jan 08 '19 at 01:21
1
actix::run(|| {
    client::get("http://ipv4.canhasip.com/")
        .header("User-Agent", "Actix-web")
        .finish()
        .unwrap()
        .send()
        .map_err(drop)
        .and_then(|response| response.body().map_err(drop))
        .map(|body| body.to_vec())
        .map(|body| String::from_utf8(body).unwrap())
        .map(drop) // Do *something* with the string, presumably
});

The result of send is a SendRequest. This is a future that resolves into a ClientResponse. ClientResponse implements HttpMessage, which has the method HttpMessage::body. This returns a future that resolves into a Bytes. This can be converted into a String through the usual Rust methods.

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • What is the meaning of `drop` in `map_err`? – Rokit Sep 29 '19 at 19:52
  • 1
    Looks like `drop` is a prelude function passed into `map_err` that throws away the error. https://doc.rust-lang.org/std/ops/trait.Drop.html https://doc.rust-lang.org/std/prelude/ – Rokit Sep 29 '19 at 20:10