Echo, echo, echo
You already have a Hello World server? Excellent! Usually, servers do more than just spit out the same body for every request. To exercise several more parts of hyper, this guide will go through building an echo server.
An echo server will listen for incoming connections and send back the
request body as the response body on POST
requests.
Routing
First thing we will do, beyond renaming our service to echo
, is setup some
routing. We want to have a route explaining instructions on how to use
our server, and another for receiving data. Oh, and we should also
handle the case when someone asks for a route we don’t know!
Before we get started we need to add some new imports:
# extern crate hyper;
# extern crate http_body_util;
use hyper::body::Frame;
use hyper::{Method, StatusCode};
use http_body_util::{combinators::BoxBody, BodyExt};
# fn main() {}
Next, we need to make some changes to our Service
function, but as you can see
it’s still just an async function that takes a Request
and returns a Response
future, and you can pass it to your server just like we did for the hello
service.
Unlike our hello
service where we didn’t care about the request body and
we always returned a single chunk of bytes containing our greeting, we’re now
going to want a bit more freedom in how we shape our response Body
. To achieve
this we will change the type of the Body
in our Response
to a boxed trait object.
We only care that the response body implements the Body trait, that its data is Bytes
and its error is a hyper::Error
.
# extern crate hyper;
# extern crate http_body_util;
# use hyper::body::Bytes;
# use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
# use hyper::{Method, Request, Response, StatusCode};
async fn echo(
req: Request<hyper::body::Incoming>,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/") => Ok(Response::new(full(
"Try POSTing data to /echo",
))),
(&Method::POST, "/echo") => {
// we'll be back
# Ok(Response::new(req.into_body().boxed()))
},
// Return 404 Not Found for other routes.
_ => {
let mut not_found = Response::new(empty());
*not_found.status_mut() = StatusCode::NOT_FOUND;
Ok(not_found)
}
}
}
// We create some utility functions to make Empty and Full bodies
// fit our broadened Response body type.
fn empty() -> BoxBody<Bytes, hyper::Error> {
Empty::<Bytes>::new()
.map_err(|never| match never {})
.boxed()
}
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
Full::new(chunk.into())
.map_err(|never| match never {})
.boxed()
}
# fn main() {}
We built a super simple routing table just by matching on the method
and path
of an incoming Request
. If someone requests GET /
, our service will let them
know they should try our echo powers out. We also check for POST /echo
, but
currently don’t do anything about it.
Our third rule catches any other method and path combination, and changes the
StatusCode
of the Response
. The default status of a Response
is HTTP’s
200 OK
(StatusCode::OK
), which is correct for the other routes. But the third
case will instead send back 404 Not Found
.
Body Streams
Now let’s get that echo in place. An HTTP body is a stream of Frames
, each
Frame containing
parts of the Body
data or trailers. So rather than reading the entire Body
into a buffer before sending our response, we can stream each frame as it arrives.
We’ll start with the simplest solution, and then make alterations exercising more complex
things you can do with the Body
streams.
First up, plain echo. Both the Request
and the Response
have body streams,
and by default, you can easily pass the Body
of the Request
into a Response
.
# extern crate hyper;
# extern crate http_body_util;
# use hyper::body::Bytes;
# use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
# use hyper::{Method, Request, Response, StatusCode};
# async fn echo(
# req: Request<hyper::body::Incoming>,
# ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
# match (req.method(), req.uri().path()) {
// Inside the match from before
(&Method::POST, "/echo") => Ok(Response::new(req.into_body().boxed())),
# _ => unreachable!(),
# }
# }
# fn main() {}
Running our server now will echo any data we POST
to /echo
. That was easy.
What if we wanted to uppercase all the text? We could use a map
on our streams.
Body mapping
Every data Frame
of our body stream is a chunk of bytes, which we can conveniently
represent using the Bytes
type from hyper. It can be easily converted into other
typical containers of bytes.
Next, let’s add a new /echo/uppercase
route, mapping each byte in the data Frame
s
of our request body to uppercase, and returning the stream in our Response
:
# extern crate hyper;
# extern crate http_body_util;
# use hyper::body::Bytes;
# use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
# use hyper::body::Frame;
# use hyper::{Method, Request, Response, StatusCode};
# async fn echo(
# req: Request<hyper::body::Incoming>,
# ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
# match (req.method(), req.uri().path()) {
// Yet another route inside our match block...
(&Method::POST, "/echo/uppercase") => {
// Map this body's frame to a different type
let frame_stream = req.into_body().map_frame(|frame| {
let frame = if let Ok(data) = frame.into_data() {
// Convert every byte in every Data frame to uppercase
data.iter()
.map(|byte| byte.to_ascii_uppercase())
.collect::<Bytes>()
} else {
Bytes::new()
};
Frame::data(frame)
});
Ok(Response::new(frame_stream.boxed()))
},
# _ => unreachable!(),
# }
# }
# fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
# Full::new(chunk.into())
# .map_err(|never| match never {})
# .boxed()
# }
# fn main() {}
And like that, we have two echo routes: /echo
which does no transformation,
and /echo/uppercase
which returns all bytes after converting them to ASCII
uppercase.
Buffering the Request Body
What if we want our echo service to reverse the data it received and send it back to us? We can’t really stream the data as it comes in, since we need to find the end before we can respond. To do this, we can explore how to easily collect the full body.
We want to collect the entire request body and map the result into our reverse
function, then return the eventual result. If we import the http_body_util::BodyExt
extension trait, we can call the collect
method on our body, which will drive the
stream to completion, collecting all the data and trailer frames into a Collected
type.
We can easily turn the Collected
body into a single Bytes
by calling its into_bytes
method.
Note: You must always be careful not to buffer without a max bounds. We’ll set a 64kb maximum here.
# extern crate hyper;
# extern crate http_body_util;
# use hyper::body::Bytes;
# use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
# use hyper::{body::Body, Method, Request, Response, StatusCode};
# async fn echo(
# req: Request<hyper::body::Incoming>,
# ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
# match (req.method(), req.uri().path()) {
// Yet another route inside our match block...
(&Method::POST, "/echo/reversed") => {
// Protect our server from massive bodies.
let upper = req.body().size_hint().upper().unwrap_or(u64::MAX);
if upper > 1024 * 64 {
let mut resp = Response::new(full("Body too big"));
*resp.status_mut() = hyper::StatusCode::PAYLOAD_TOO_LARGE;
return Ok(resp);
}
// Await the whole body to be collected into a single `Bytes`...
let whole_body = req.collect().await?.to_bytes();
// Iterate the whole body in reverse order and collect into a new Vec.
let reversed_body = whole_body.iter()
.rev()
.cloned()
.collect::<Vec<u8>>();
Ok(Response::new(full(reversed_body)))
},
# _ => unreachable!(),
# }
# }
# fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
# Full::new(chunk.into())
# .map_err(|never| match never {})
# .boxed()
# }
# fn main() {}
You can see a compiling example here.