• Stars
    star
    326
  • Rank 129,027 (Top 3 %)
  • Language
    Rust
  • Created over 9 years ago
  • Updated almost 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

An example of a Rust web service with Iron and Hyper

Let's make a web service and client in Rust

So I'm working on this project Crater for doing Rust regression testing. Looking into the feasibility of writing parts in Rust. I need an HTTP server that speaks JSON, along with a corresponding client. I don't see a lot of docs on how to do this so I'm recording my investigation.

I'm going to use Iron and Hyper, neither of which I have experience with.

Each commit in this repo corresponds with a chapter, so follow along if you want.

Edit: Some inaccuracies within! Thanks /r/rust!

1. Preparing to serve some JSON

I start by asking Cargo to give me a new executable project called 'httptest'. Passing --bin says to create a source file in src/main.rs that will be compiled to an application.

$ cargo new httptest --bin

I'll use Iron for the server, so following the instructions in their docs add the following to my Cargo.toml file.

[dependencies]
iron = "*"

Then into main.rs I just copy their example.

extern crate iron;

use iron::prelude::*;
use iron::status;

fn main() {
    fn hello_world(_: &mut Request) -> IronResult<Response> {
        Ok(Response::with((status::Ok, "Hello World!")))
    }

    Iron::new(hello_world).http("localhost:3000").unwrap();
    println!("On 3000");
}

And type cargo build.

$ cargo build
Compiling hyper v0.3.13
Compiling iron v0.1.16
Compiling httptest v0.2.0 (file:///opt/dev/httptest)

Pure success so far. Damn, Rust is smooth. Let's try running the server with cargo run.

$ cargo run
Running `target/debug/httptest`

I sit here waiting a while expecting it to print "On 3000" but it never does. Cargo must be capturing output. Let's see if we're serving something.

$ curl http://localhost:3000
Hello World!

Oh, that's super cool. We know how to build a web server now. Good starting point.

2. Serving a struct as JSON

Is rustc-serialize still the easiest way to convert to and from JSON? Maybe I should use serde, but then you really want serde_macros, but that only works on Rust nightlies. Should I just use nightlies? Nobody else is going to need to use this.

Let's just go with the tried-and-true rustc-serialize. Now my Cargo.toml 'dependencies' section looks like the following.

[dependencies]
iron = "*"
rustc-serialize = "*"

Based on the rustc-serialize docs I update main.rs to look like this:

extern crate iron;
extern crate rustc_serialize;

use iron::prelude::*;
use iron::status;
use rustc_serialize::json;

#[derive(RustcEncodable)]
struct Greeting {
    msg: String
}

fn main() {
    fn hello_world(_: &mut Request) -> IronResult<Response> {
        let greeting = Greeting { msg: "Hello, World".to_string() };
        let payload = json::encode(&greeting).unwrap();
        Ok(Response::with((status::Ok, payload)))
    }

    Iron::new(hello_world).http("localhost:3000").unwrap();
    println!("On 3000");
}

And then run cargo build.

$ cargo build
   Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading unicase v1.1.1
 <... more downloading here ...>
   Compiling lazy_static v0.1.15
 <... more compiling here ...>
   Compiling httptest v0.2.0 (file:///opt/dev/httptest)
src/main.rs:17:12: 17:47 error: this function takes 1 parameter but 2 parameters were supplied [E0061]
src/main.rs:17         Ok(Response::with(status::Ok, payload))
                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.rs:17:12: 17:47 help: run `rustc --explain E0061` to see a detailed explanation
error: aborting due to previous error
Could not compile `httptest`.

To learn more, run the command again with --verbose.

Lot's of new dependencies now. But an error. Response::with has this definition:

fn with<M: Modifier<Response>>(m: M) -> Response

I don't know what a Modifier is. Some docs don't help. I don't know what to do here but I notice that the original example passed a tuple to Response::with whereas my update treated Response::with as taking two parameters. It seems a tuple is a Modifier.

Add the tuple to Ok(Response::with((status::Ok, payload))), execute cargo run, curl some JSON.

$ curl http://localhost:3000
{"msg":"Hello, World"}

Blam! We're sending JSON. Time for another break.

3. Routes

Next I want to POST some JSON, but before I do that I need a proper URL to post to, so I guess I need to learn how to set up routes.

I look at the Iron docs and don't see anything obvious in the main text, but there's a crate called router that might be interesting.

The module docs are "Router provides fast and flexible routing for Iron", but not much else. How do I use a Router?! After I lot of sleuthing I discover an example. OK, let's try to adapt that to our evolving experiment.

I add router = "*" to my Cargo.toml [dependencies] section, and begin writing. The following is what I come up with before getting stuck reading the POST data.

extern crate iron;
extern crate router;
extern crate rustc_serialize;

use iron::prelude::*;
use iron::status;
use router::Router;
use rustc_serialize::json;

#[derive(RustcEncodable, RustcDecodable)]
struct Greeting {
    msg: String
}

fn main() {
    let mut router = Router::new();

    router.get("/", hello_world);
    router.post("/set", set_greeting);

    fn hello_world(_: &mut Request) -> IronResult<Response> {
        let greeting = Greeting { msg: "Hello, World".to_string() };
        let payload = json::encode(&greeting).unwrap();
        Ok(Response::with((status::Ok, payload)))
    }

    // Receive a message by POST and play it back.
    fn set_greeting(request: &mut Request) -> IronResult<Response> {
        let payload = request.body.read_to_string();
        let request: Greeting = json::decode(payload).unwrap();
        let greeting = Greeting { msg: request.msg };
        let payload = json::encode(&greeting).unwrap();
        Ok(Response::with((status::Ok, payload)))
    }

    Iron::new(router).http("localhost:3000").unwrap();
}

This uses Router to control handler dispatch. It builds and still responds to curl http://localhost:3000, but the handler for the /set route is yet unimplemented.

Now to read the POST body into a string. The docs for Request say the field body is an iterator, so we just need to collect that iterator into a string.

I first try let payload = request.body.read_to_string(); because I know it used to work.

It does not work.

$ cargo build
Compiling httptest v0.2.0 (file:///opt/dev/httptest)
src/main.rs:29:36: 29:52 error: no method named `read_to_string` found for type `iron::request::Body<'_, '_>` in the current scope
src/main.rs:29         let payload = request.body.read_to_string();
                                                  ^~~~~~~~~~~~~~~~
src/main.rs:29:36: 29:52 help: items from traits can only be used if the trait is in scope; the following trait is implemented but not in scope, perhaps add a `use` for it:
src/main.rs:29:36: 29:52 help: candidate #1: use `std::io::Read`
error: aborting due to previous error
Could not compile `httptest`.

To learn more, run the command again with --verbose.

I throw my hands up in disgust. 'Why does this method no longer exist? The Rust team is always playing tricks on us!' Then I notice the compiler has - at some length - explained that the method does exist and that I should import std::io::Read.

I add the import and discover that read_to_string behaves differently than I thought.

101 $ cargo build
Compiling httptest v0.2.0 (file:///opt/dev/httptest)
src/main.rs:30:36: 30:52 error: this function takes 1 parameter but 0 parameters were supplied [E0061]
src/main.rs:30         let payload = request.body.read_to_string();
                                                  ^~~~~~~~~~~~~~~~

Ok, yeah the signature of Read::read_to_string is now fn read_to_string(&mut self, buf: &mut String) -> Result<usize, Error>, so that the buffer is supplied and errors handled. Rewrite the set_greeting method.

    // Receive a message by POST and play it back.
    fn set_greeting(request: &mut Request) -> IronResult<Response> {
        let mut payload = String::new();
        request.body.read_to_string(&mut payload).unwrap();
        let request: Greeting = json::decode(&payload).unwrap();
        let greeting = Greeting { msg: request.msg };
        let payload = json::encode(&greeting).unwrap();
        Ok(Response::with((status::Ok, payload)))
    }

Let's run this and give it a curl.

$ curl -X POST -d '{"msg":"Just trust the Rust"}' http://localhost:3000/set
{"msg":"Just trust the Rust"}

Oh, Rust. You're just too bad.

4. Mutation

Hey, I know all those .unwrap()s are wrong. I don't care. We're prototyping.

Before we continue on to writing a client, I want to modify this toy example to store some state on POST to /set and report it later. I'll make greeting a local, capture it in some closures, then see how the compiler complains.

Here's my new main function, before attempting to compile:

fn main() {
    let mut greeting = Greeting { msg: "Hello, World".to_string() };

    let mut router = Router::new();

    router.get("/", |r| hello_world(r, &greeting));
    router.post("/set", |r| set_greeting(r, &mut greeting));

    fn hello_world(_: &mut Request, greeting: &Greeting) -> IronResult<Response> {
        let payload = json::encode(&greeting).unwrap();
        Ok(Response::with((status::Ok, payload)))
    }

    // Receive a message by POST and play it back.
    fn set_greeting(request: &mut Request, greeting: &mut Greeting) -> IronResult<Response> {
        let mut payload = String::new();
        request.body.read_to_string(&mut payload).unwrap();
        *greeting = json::decode(&payload).unwrap();
        Ok(Response::with(status::Ok))
    }

    Iron::new(router).http("localhost:3000").unwrap();
}
src/main.rs:21:12: 21:51 error: type mismatch resolving `for<'r, 'r, 'r> <[closure@src/main.rs:21:21: 21:50 greeting:_] as core::ops::FnOnce<(&'r mut iron::request::Request<'r, 'r>,)>>::Output == core::result::Result<iron::response::Response, iron::error::IronError>`:
 expected bound lifetime parameter ,
    found concrete lifetime [E0271]
src/main.rs:21     router.get("/", |r| hello_world(r, &greeting));
                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.rs:21:12: 21:51 help: run `rustc --explain E0271` to see a detailed explanation
src/main.rs:22:12: 22:60 error: type mismatch resolving `for<'r, 'r, 'r> <[closure@src/main.rs:22:25: 22:59 greeting:_] as core::ops::FnOnce<(&'r mut iron::request::Request<'r, 'r>,)>>::Output == core::result::Result<iron::response::Response, iron::error::IronError>`:
 expected bound lifetime parameter ,
    found concrete lifetime [E0271]
src/main.rs:22     router.post("/set", |r| set_greeting(r, &mut greeting));
                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.rs:22:12: 22:60 help: run `rustc --explain E0271` to see a detailed explanation
src/main.rs:21:12: 21:51 error: type mismatch: the type `[closure@src/main.rs:21:21: 21:50 greeting:&Greeting]` implements the trait `core::ops::Fn<(&mut iron::request::Request<'_, '_>,)>`, but the trait `for<'r, 'r, 'r> core::ops::Fn<(&'r mut iron::request::Request<'r, 'r>,)>` is required (expected concrete lifetime, found bound lifetime parameter ) [E0281]
src/main.rs:21     router.get("/", |r| hello_world(r, &greeting));
                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.rs:21:12: 21:51 help: run `rustc --explain E0281` to see a detailed explanation
src/main.rs:22:12: 22:60 error: the trait `for<'r, 'r, 'r> core::ops::Fn<(&'r mut iron::request::Request<'r, 'r>,)>` is not implemented for the type `[closure@src/main.rs:22:25: 22:59 greeting:&mut Greeting]` [E0277]
src/main.rs:22     router.post("/set", |r| set_greeting(r, &mut greeting));
                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.rs:22:12: 22:60 help: run `rustc --explain E0277` to see a detailed explanation
error: aborting due to 4 previous errors
Could not compile `httptest`.

rustc is not happy. But I expected that. I'm throwing types at it just to get a response. Tell me what to do rustc.

Those error messages are confusing, but clearly the closure is the wrong type. The get and post methods of Router take a Handler, and from the doc page I see there's an impl defined as

pub trait Handler: Send + Sync + Any {
    fn handle(&self, &mut Request) -> IronResult<Response>;
}

That's a mouthful, but Handler is defined for Fn, not FnOnce or FnMut, and it has to be Send + Sync. Since it needs to be send, we're not going to be capturing any references, and since the environment isn't mutable we have to use interior mutability to mutate the greeting. So I'm going to use a sendable smart pointer, Arc, and to make it mutable, put a Mutex inside it. For that we need to import both from the standard library like this: use std::sync::{Mutex, Arc};. I'm also going to have to move the captures with move |r| ... to avoid capturing by reference.

Updating my code like so yields the same error messages.

    let greeting = Arc::new(Mutex::new(Greeting { msg: "Hello, World".to_string() }));
    let greeting_clone = greeting.clone();

    let mut router = Router::new();

    router.get("/", move |r| hello_world(r, &greeting.lock().unwrap()));
    router.post("/set", move |r| set_greeting(r, &mut greeting_clone.lock().unwrap()));

rustc doesn't like the lifetimes of my closure. Why? I don't know. I ask reem in #rust if he knows what to do.

Several hours later reem says

16:02 < reem> brson: Partially hint the type of the request art, rustc has trouble inferring HRTBs

HRTB means 'higher-ranked trait bounds', which means roughly 'complicated lifetimes'.

I change those same lines to hint the type of r: &mut Request and everything works...

    let greeting = Arc::new(Mutex::new(Greeting { msg: "Hello, World".to_string() }));
    let greeting_clone = greeting.clone();

    let mut router = Router::new();

    router.get("/", move |r: &mut Request| hello_world(r, &greeting.lock().unwrap()));
    router.post("/set", move |r: &mut Request| set_greeting(r, &mut greeting_clone.lock().unwrap()));

It was seemingly a bug in Rust's inferencer. That's lame.

Now it builds again, so we can test with curl.

$ curl http://localhost:3000
{"msg":"Hello, World"}
$ curl -X POST -d '{"msg":"Just trust the Rust"}' http://localhost:3000/set
$ curl http://localhost:3000
{"msg":"Just trust the Rust"}

Now we're playing with power.

5. The client

We've got a little JSON server going. Now let's write the client. This time we're going to use Hyper directly.

I add it to my Cargo.toml: hyper = "*", then create src/bin/client.rs:

extern crate hyper;

fn main() { }

Source files in src/bin/ are automatically built as executables by cargo. Run cargo build and the target/debug/client program appears. Good, universe is sane. Now figure out Hyper.

Cribbing off the Hyper client example I come up with this snippet that just makes a request for "/" and prints the body:

extern crate hyper;

use hyper::*;
use std::io::Read;

fn main() {
    let client = Client::new();
    let mut res = client.get("http://localhost:3000/").send().unwrap();
    assert_eq!(res.status, hyper::Ok);
    let mut s = String::new();
    res.read_to_string(&mut s).unwrap();
    println!("{}", s);
}

But now cargo run no longer works.

$ cargo run
`cargo run` requires that a project only have one executable; use the `--bin` option to specify which one to run

I must type cargo run --bin httptest to start the server. I do so, then cargo run --bin client and see

$ cargo run --bin client
Running `target/debug/client`
{"msg":"Hello, World"}

Oh, man, I'm a Rust wizard. One last thing I want to do, make the POST request to set the message. Obvious thing to do is change client.get to client.post. This returns a RequestBuilder, so I'm looking for a builder method that sets the payload. How about body?

My new creation:

extern crate hyper;

use hyper::*;
use std::io::Read;

fn main() {
    let client = Client::new();
    let res = client.post("http://localhost:3000/set").body("Just trust the Rust").send().unwrap();
    assert_eq!(res.status, hyper::Ok);
    let mut res = client.get("http://localhost:3000/").send().unwrap();
    assert_eq!(res.status, hyper::Ok);
    let mut s = String::new();
    res.read_to_string(&mut s).unwrap();
    println!("{}", s);
}

But running it is disappointing.

101 $ cargo run --bin client
   Compiling httptest v0.2.0 (file:///Users/danieleesposti/workspace/httptest)
     Running `target/debug/client`
thread '<main>' panicked at 'assertion failed: `(left == right)` (left: `InternalServerError`, right: `Ok`)', src/bin/client.rs:9

And simultaneously I see that the server has also errored:

$ cargo run --bin httptest
   Running `target/debug/httptest`
thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: ParseError(SyntaxError("invalid syntax", 1, 1))', src/libcore/result.rs:741

It's because of my bad error handling! I didn't pass valid JSON to the /set route. Fixing the body to be .body(r#"{ "msg": "Just trust the Rust" }"#) lets the client succeed:

$ cargo run --bin client
   Compiling httptest v0.2.0 (file:///opt/dev/httptest)
      Running `target/debug/client`
{"msg":"Just trust the Rust"}

And just like that we've created a web service and client in Rust. Looks like the future is near. Go build something with Iron and Hyper.

More Repositories

1

rust-anthology

Learn Rust from the best
Shell
1,353
star
2

multirust

[DEPRECATED] A tool for managing multiple Rust installations
Shell
656
star
3

my-rust-lists

Lightly organized personal notes about Rust
445
star
4

basic-http-server

A simple static HTTP server in Rust, for learning and local doc development
Rust
371
star
5

rust-sdl

SDL bindings for Rust
Rust
179
star
6

annotated-std-rs

An annotation of the Rust standard library
71
star
7

home

Canonical definitions of home_dir, CARGO_HOME, and RUSTUP_HOME
Rust
65
star
8

wasm-opt-rs

Rust bindings for Binaryen's wasm-opt
WebAssembly
63
star
9

cargo-crusader

Test the downstream impact of Rust crate changes before publishing
Rust
59
star
10

treasuretree

A real-world treasure hunt game where the treasures are represented as QR codes in the physical world, and NFTs in the virtual.
Rust
51
star
11

ferris-fencing

Rust
42
star
12

rustle

Install a Cargo application from source without a preexisting Rust installation
Shell
37
star
13

rust-in-china

JavaScript
36
star
14

the-end-of-unsafety

https://brson.github.io/the-end-of-unsafety
JavaScript
33
star
15

rust-chamber

Rust as sandbox
Rust
32
star
16

burst

Realtime programming for Rust
Rust
27
star
17

temple-of-rust

Tutum. Simul. Celerem.
CSS
23
star
18

archaea

Historic Rust code browsing
JavaScript
22
star
19

rustray

Sebastian Sylvan's Rust ray-tracer
Rust
20
star
20

big_s

Rust's missing `String` literal
Rust
19
star
21

being-rust

Intro to Rust talk
JavaScript
15
star
22

rubyrustdemo

JavaScript
14
star
23

brson.github.com

JavaScript
11
star
24

paxos-for-dummies-like-me

Rust
11
star
25

beast

Go
10
star
26

rust-is-for-blockchain

Slides about the Rust in the blockchain industry
JavaScript
10
star
27

stabworld

Tools for analyzing how the Rust ecosystem is using the language
Shell
10
star
28

zenburn-konsole

Zenburn for Konsole
8
star
29

contract-game

Rust
8
star
30

rust-infra

Information about Rust infrastructure
8
star
31

og_fmt

Will the real `format!` please stand up?
Rust
7
star
32

hackmd-collab

7
star
33

megamalloc

Rust
6
star
34

rust-sha2

SHA-2 in pure Rust
Rust
6
star
35

cargo-bake

Smarter default optimization settings for Rust + Cargo
Rust
6
star
36

blocksy3

An in-process key value store.
Rust
6
star
37

ctrs

A Rust language conformance test suite
Rust
5
star
38

cargo-lock-to-dot

Generate a GraphViz DOT file from a Rust Cargo.lock file
Python
5
star
39

pwrabs

Rust
5
star
40

ragel-dist

ragel repo used by servo
C++
5
star
41

fireflowers

HTML
5
star
42

bloop

Rust
4
star
43

synth2

Rust
4
star
44

heka-rs

Originally https://bitbucket.org/trink/heka_rs
Rust
4
star
45

rust-contract-comparison

JavaScript
3
star
46

rust-llvm

LLVM bindings for Rust
Rust
3
star
47

slush

C#
3
star
48

rust-dev-archives

Archives of the rust-dev mailing list
3
star
49

worklog-scan

Rust
3
star
50

soroban-token-fuzzer

Rust
3
star
51

cargobomb-reports

2
star
52

project-servo-talk

Backup of Graydon's original Servo talk
2
star
53

devscripts

Scripts for configuring my dev environment
Shell
2
star
54

rust-irc-logs

Historic Rust IRC logs
2
star
55

fancy_flocks

Rust
2
star
56

quickshake

Shrinks Java libraries fast
Scala
2
star
57

solana-move-demo

Move
2
star
58

mem-basics

Rust
2
star
59

blocksy2

Rust
1
star
60

tryrust

JavaScript
1
star
61

rust-art

1
star
62

blip

C
1
star
63

rust-hello

Hello World in rust.
Rust
1
star
64

wamalama

Rust
1
star
65

atomic_blobject

Rust
1
star
66

load-maker

Load up your CPUs
Rust
1
star
67

rustzlib

zlib bindings for rust
Rust
1
star
68

parse_list

Parse files and lists of stringified things into lists of thingified things
Rust
1
star
69

rusty-cairo

Cairo bindings for Rust, enabling a powerful interface to vector graphics.
Rust
1
star
70

dochub

JavaScript
1
star
71

beagle-delicious

C#
1
star
72

bench-cargo-profiles

Rust
1
star
73

rustscripten

Tools for building Rust code with emscripten
JavaScript
1
star
74

wasm-encrypted-pastebin

1
star
75

stoptime

Scala
1
star
76

slide-deck-template

JavaScript
1
star
77

result_iter

Rust
1
star
78

servo-pages

Hosted servo test cases
JavaScript
1
star
79

harfbuzz-servo

C++
1
star
80

euler

Haskell
1
star
81

instant-httpd

A simple script to serve static web pages from the current directory with no configuration
Python
1
star
82

coconuts

Jupyter Notebook
1
star
83

rustpcre

PCRE bindings for Rust
Rust
1
star
84

blocksy

Rust
1
star
85

rwh

Haskell
1
star
86

ckb-vm-rs-tests

Rust
1
star
87

memstuff

Rust
1
star
88

servo-buildbot

1
star
89

cargo-index-tool

Rust
1
star
90

ckb-rpc-test

Rust
1
star
91

gyp

Clone of gyp for Rust
Python
1
star
92

rust_glfw

glfw bindings for the rust programming language
Rust
1
star
93

audiotool

Rust
1
star
94

slq

Rust
1
star