Web fundamental series—2 Link to heading

Today, let’s examine what a real-world request from a browser might look like. We will build our server to display two text area forms with a button. Upon a user entering texts and submitting, our server will simply print out the request and do nothing. This way, we know how to parse the data.

So, the server will display the following page to the user.

Server Page

For that, our Rust server code looks like this

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};

const OK: &str = "HTTP/1.1 200 OK\r\n\r\n";
const HTML: &str = r#"
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
    <div>
        <form action="/" method="get">
            <table>
                <tr>
                    <td>
                        <label for="string1">String1</label>
                    </td>
                    <td>
                        <label for="string2">String2</label>
                    </td>
                </tr>
                <tr>
                    <td>
                        <textarea id="string1" name="string1"></textarea>
                    </td>
                    <td>
                        <textarea id="string2" name="string2"></textarea>
                    </td>
                </tr>
            </table>
            <div>
                <button type="submit">Submit</button>
            </div>
        </form>
    </div>
</body>
</html>"#;

fn handle_client(mut stream: TcpStream) -> std::io::Result<()> {
    let packet = read_packet(&mut stream)?;
    let request = String::from_utf8_lossy(&packet);
    println!("{}", request);

    let response = format!("{OK}{HTML}");
    stream.write_all(response.as_bytes())?;
    stream.flush()?;
    Ok(())
}

fn read_packet(stream: &mut TcpStream) -> std::io::Result<Vec<u8>> {
    const BUFFER_SIZE: usize = 8 << 10; // 8k
    let mut buffer = [0; BUFFER_SIZE];
    let mut packet = Vec::new();
    loop {
        if let Ok(n) = stream.read(&mut buffer) {
            packet.extend_from_slice(&buffer[..n]);
            if n < BUFFER_SIZE {
                break;
            }
        }
    }
    Ok(packet)
}

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080")?;
    println!("Listening on port 8080");

    for stream in listener.incoming() {
        let stream = stream?;
        handle_client(stream)?;
    }
    Ok(())
}

The serve always returns the same HTML page, but prints outs every request it receives. When opened the page in the browser and submitted the form with string1 = “abc ” and string2 = “!@#”, the web server prints the following browser request:

Browser Request

Worth mentioning are

  • The request is GET method, as indicated by the HTML
  • The path value in the request line (first line) shows the key-value pairs in the URL-encoded format. This is called query string. This is a convenient way to encode parameters into the URL

With this, we will modify our server to receive such request and be able to parse the data successfully. As for the query string, let’s use serde_urlencoded crate to decode.

--- before
+++ after
@@ -1,7 +1,10 @@
+use std::collections::HashMap;
 use std::io::{Read, Write};
 use std::net::{TcpListener, TcpStream};
+use serde_urlencoded;

 const OK: &str = "HTTP/1.1 200 OK\r\n\r\n";
+const NOT_FOUND: &str = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
 const HTML: &str = r#"
 <!DOCTYPE html>
 <html lang="en">
@@ -38,14 +41,35 @@ fn handle_client(mut stream: TcpStream) -> std::io::Result<()> {
     let packet = read_packet(&mut stream)?;
     let request = String::from_utf8_lossy(&packet);
-    println!("{}", request);
+    let mut it = request.split(' ');
+    let response = match it.next() {
+        Some("GET") => {
+            let path = it.next().unwrap();
+            let map = parse_query_string(path);
+            match (map.get("string1"), map.get("string2")) {
+                (Some(s1), Some(s2)) => format!("{OK}received string1={} and string2={}\n", s1, s2),
+                _ => format!("{OK}{HTML}"),
+            }
+        }
+        _ => NOT_FOUND.to_owned(),
+    };

-    let response = format!("{OK}{HTML}");
     stream.write_all(response.as_bytes())?;
     stream.flush()?;
     Ok(())
 }

+fn parse_query_string(qs: &str) -> HashMap<String, String> {
+    let mut map = HashMap::new();
+    if let Some(queries) = qs.split("?").skip(1).next() {
+        for pair in serde_urlencoded::from_str::<Vec<(String, String)>>(&queries).unwrap() {
+            map.insert(pair.0, pair.1);
+        }
+    }
+
+    map
+}
+

Now, upon submitting the form, the server now acknowledges the inputs.

Server Response