Part of the Tiny HTTP series.
This is a very simple application. When we visit the web site we see a single box where we can type in some text and a button.
When we click the button the server show the same box and button and it also shows You typed and the text we typed in.
This already shows a full round-trip processing of HTTP request and response.
In order to make this work we had to add the url crate so our Cargo.toml
now looks like this:
examples/tiny-http/echo-get/Cargo.toml
[package]
name = "echo-get"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ascii = "1.1"
tiny_http = "0.12"
url = "2.5"
The source code
examples/tiny-http/echo-get/src/main.rs
use ascii::AsciiString;
use std::collections::HashMap;
use std::str::FromStr;
use tiny_http::{Header, HeaderField, Response, Server};
fn main() {
let host = "127.0.0.1";
let port = "5000";
let server_str = format!("{}:{}", host, port);
let server = Server::http(&server_str).expect("Failed to start demo server.");
for request in server.incoming_requests() {
let header = Header {
field: HeaderField::from_str("Content-type").unwrap(),
value: AsciiString::from_ascii("text/html").unwrap(),
};
let mut html = String::from(
r#"<h1>Echo</h1>
<form>
<input type="text" name="text">
<input type="submit" value="Echo">
</form>
"#,
);
let params = get_url_parameters(&request);
// println!("params: {:?}", params);
if params.contains_key("text") {
let text = ¶ms["text"][0];
if !text.is_empty() {
html.push_str(format!("You typed: '{}'", text).as_str());
}
}
request
.respond(Response::from_string(html).with_header(header))
.unwrap();
}
}
fn get_url_parameters(request: &tiny_http::Request) -> HashMap<String, Vec<String>> {
// TODO get rid of this fake URL thing
let fake_url = format!("http://localhost{}", request.url().to_owned());
// println!("full_url: {fake_url}");
let req = url::Url::parse(&fake_url).unwrap();
// println!("path: {}", req.path());
let mut query: HashMap<String, Vec<String>> = HashMap::new();
for (field, value) in req.query_pairs() {
let field = field.to_string();
let value = value.to_string();
if !query.contains_key(&field) {
query.insert(field.clone(), vec![]);
}
query
.entry(field)
.and_modify(|vector| vector.push(value.to_owned()));
}
query
}
The explanation
There is a function now called get_url_parameters
that receives a reference to a tiny_http::Request.
The url method will return the url starting from the initial /
.
Excluding the protocol that is either http
or https
, excluding the hostname and excluding the port number. So if we browse to this address:
http://localhost:5000/api/echo?text=Hello+World!&name=Foo%20Bar&text=more%20info
The url
method will return
/api/echo?text=Hello+World!&name=Foo%20Bar&text=more%20info
From this we need to extract the part that is after the ?
that are the parameters.
I could not figure out how to feed this string to the url crate so I created a fake_url
and passed that to the url::Url::parse method.
Using that we go over the pairs returned by query_pairs and we build a hash of vectors. Why vectors and not single values? Because one can supply the same key multiple times on the URL. In our example above we used the "text" field name twice.
I think I could have decided to use the first or the last value in case there are multiple values, but accepting more than one possible values seemed like a more correct solution. This of course will mean that the user of this data structure will have to handle one or more values.
Given this request http://localhost:5000/?text=Hello+World!&name=Foo%20Bar&text=more%20info
, these are the params:
{
"name": ["Foo Bar"],
"text": ["Hello World!", "more info"]
}
Now let's see the body of the for request in server.incoming_requests()
loop.
-
We create the header to be able to set the Content-type to
text/html
as we have already seen in the Hello World example. -
The we create a string called
html
with the HTML form. Embedding HTML in our Rust code is a nasty solution, but I did not want to deal with template system at this point. The variable was made mutable so it will be possible to add the response text. -
Then we call
get_url_parameters
described earlier to get the parameters from the URL. -
Then we have the code to add the reply. When we first visit the web site at
http://localhost:5000/
as you can see in the first image, there are no parameters. The params variable will be empty, thetext
field won't exists and thus the block of theif
-statement will be skipped. Once we type in some text in the box and click on the link the HTML form will be sent to the same address, but this time the URL will also include the?text=Hello+World!
part as you can see in the second image. This will be parsed by theget_url_parameters
function and we will have atext
key in theparams
variable that will contain a vector of strings. We take the first one and append it to thehtml
variable with some extra text. -
The last 3 lines in the
main
function returns the html string with the header.