formsrustserverform-datatcpstream

Missing content from TcpStream Buffer in Rust in all but multpart form data


I have a local RUST server I'm building, and I'm using a local rest client to send messages to it. It just has some test output to console at the moment, and my function looks something like this:

fn handle_connection(mut stream: TcpStream){
    let mut buf_reader = BufReader::with_capacity(16384, &mut stream);
    let mut request_lines = buf_reader.lines().peekable();
    let first_request_line = request_lines.next().unwrap().unwrap();
    let parts: Vec<&str> = first_request_line[..].split(" ").collect();
    if (parts[0] == "POST" && parts[2] == "HTTP/1.1") {
        let mut buffer_results = HashMap::new();
        let mut i = 0;
        while (request_lines.peek().is_some()){
                let newline = request_lines.next().unwrap().unwrap();
                let newline_copy = newline.clone();
                if newline_copy == "" {
                    continue;
                } else if newline_copy.contains(":"){
                    let newline_string = newline_copy.clone().to_string().to_owned();
                    let newline_str = string_to_static_str(newline_string);
                    let templine: Vec<&str>  = (newline_str.split(":")).clone().collect();
                    let key = templine[0];
                    let value = templine[1..].join(":");
                    buffer_results.insert(key, value.clone());
                    println!("Continuing Stream {}:{}", key, value);
                } else {
                    let newline_clone = newline_copy.clone().to_string().to_owned();
                    let i_i = i32_to_static_str(i.clone());
                    buffer_results.insert(i_i, newline_clone);
                    println!("Continuing Stream {}:{:#?}", i.to_string(), newline_copy.to_string());
                }
                i += 1;
        }
...

And I do a REST using RESTED that looks like this:

Basic Rest call using some filler content

However, the console output only contains headers, no content:

POST CALL:[
    "POST",
    "/",
    "HTTP/1.1",
]
Continuing Stream Host: 127.0.0.1:7878
Continuing Stream User-Agent: [[User-Agent]]
Continuing Stream Accept: HTTP/1.1
Continuing Stream Accept-Language: [[Language]],en;q=0.5
Continuing Stream Accept-Encoding: gzip, deflate, br, zstd
Continuing Stream content-type: application/x-www-form-urlencoded
Continuing Stream Content-Length: 27
Continuing Stream Origin: moz-extension://[[HASH]]
Continuing Stream Connection: keep-alive
Continuing Stream Sec-Fetch-Dest: empty
Continuing Stream Sec-Fetch-Mode: cors
Continuing Stream Sec-Fetch-Site: same-origin
Continuing Stream Priority: u=0

I cant fetch the data if I switch to multi-part form as the request type:

Continuing Stream content-type: multipart/form-data
Continuing Stream 13:"------[[hash]]"
Continuing Stream Content-Disposition: form-data; name="my_silly_request"
Continuing Stream 15:"void_angel"
Continuing Stream 16:"------[[hash]]--"

But if I use JSON or URLencoded Form, it gives the incomplete version above.
I've tried switching from default BufReader::new() to ::with_capaicty() with a larger capacity to hopefully bring in more lines, no success. I've tried adding more variables at the end assigned with request_lines.next().unwrap().unwrap(); to hopefully catch some lines that weren't in the buffer yet when my function started with no success. I tried doing buffer.read() and just got some errors about nothing being there.

So I'm assuming its something different about the transmission of these two form types I'm missing, but I have no clue what. I feel I should know, but my brain & search bar just aren't giving me answers.


Solution

  • You're using .lines().next() which waits for a line ending before returning:

    #[stable(feature = "rust1", since = "1.0.0")]
    impl<B: BufRead> Iterator for Lines<B> {
        type Item = Result<String>;
    
        fn next(&mut self) -> Option<Result<String>> {
            let mut buf = String::new();
            match self.buf.read_line(&mut buf) {
                Ok(0) => None,
                Ok(_n) => {
                    if buf.ends_with('\n') {
                        buf.pop();
                        if buf.ends_with('\r') {
                            buf.pop();
                        }
                    }
                    Some(Ok(buf))
                }
                Err(e) => Some(Err(e)),
            }
        }
    }
    
    #[stable(feature = "rust1", since = "1.0.0")]
    fn read_line(&mut self, buf: &mut String) -> Result<usize> {
        unsafe { append_to_string(buf, |b| read_until(self, b'\n', b)) }
    }
    

    If you set a read timeout on the Stream, and wait until it times out, you'll see that the multipart form data has a line ending and the URL encoded form doesn't.

    use std::{
        io::{prelude::*},
        net::{TcpListener, TcpStream}, time::Duration,
    };
    
    fn handle_connection(mut stream: &TcpStream) {
        stream.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
        let mut buffer = String::new();
        stream.read_to_string(&mut buffer).unwrap();
    
        println!("Request: {buffer:#?}\n");
    }
    
    fn main() {
        let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
        for stream in listener.incoming() {
            handle_connection(&stream.unwrap());
        }
    }
    
    Request: "POST / HTTP/1.1\r\nHost: 127.0.0.1:7878\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0\r\nAccept: */*\r\nAccept-Language: en-GB,en;q=0.5\r\nAccept-Encoding: gzip, deflate, br, zstd\r\ncontent-type: application/x-www-form-urlencoded\r\nContent-Length: 27\r\nOrigin: moz-extension://739eb095-cac3-45a9-be66-a909f910c0b2\r\nConnection: keep-alive\r\nSec-Fetch-Dest: empty\r\nSec-Fetch-Mode: cors\r\nSec-Fetch-Site: same-origin\r\nPriority: u=0\r\n\r\nmy_silly_request=void_angel"
    
    Request: "POST / HTTP/1.1\r\nHost: 127.0.0.1:7878\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0\r\nAccept: */*\r\nAccept-Language: en-GB,en;q=0.5\r\nAccept-Encoding: gzip, deflate, br, zstd\r\ncontent-type: multipart/form-data\r\nContent-Length: 187\r\nOrigin: moz-extension://739eb095-cac3-45a9-be66-a909f910c0b2\r\nConnection: keep-alive\r\nSec-Fetch-Dest: empty\r\nSec-Fetch-Mode: cors\r\nSec-Fetch-Site: same-origin\r\nPriority: u=0\r\n\r\n------geckoformboundary2de326eb804c2dc5c34c7fdd2d3d4e52\r\nContent-Disposition: form-data; name=\"my_silly_request\"\r\n\r\nvoid_angel\r\n------geckoformboundary2de326eb804c2dc5c34c7fdd2d3d4e52--\r\n"
    

    If you refresh the RESTED Client page in the middle of the URL encoded request (while using your code), you might actually see the data come through.

    Continuing Stream Host: 127.0.0.1:7878
    Continuing Stream User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
    Continuing Stream Accept: */*
    Continuing Stream Accept-Language: en-GB,en;q=0.5
    Continuing Stream Accept-Encoding: gzip, deflate, br, zstd
    Continuing Stream content-type: application/x-www-form-urlencoded
    Continuing Stream Content-Length: 27
    Continuing Stream Origin: moz-extension://739eb095-cac3-45a9-be66-a909f910c0b2
    Continuing Stream Connection: keep-alive
    Continuing Stream Sec-Fetch-Dest: empty
    Continuing Stream Sec-Fetch-Mode: cors
    Continuing Stream Sec-Fetch-Site: same-origin
    Continuing Stream Priority: u=0
    <refresh RESTED Client page here>
    Continuing Stream 13:"my_silly_request=void_angel"
    

    If you want to know when to finish reading a URL encoded request, using the Content-Length header might be a good idea e.g.:

    use std::{
        io::{BufRead, BufReader, Read},
        net::{TcpListener, TcpStream},
    };
    
    fn handle_connection(stream: &TcpStream) {
        let mut buf_reader = BufReader::new(stream);
        let request_lines: Vec<String> = buf_reader
            .by_ref() // Used so lines() doesn't consume the reader
            .lines()
            .map(|result| result.unwrap())
            .take_while(|line| !line.is_empty()) // Read until an empty line (e.g. before URL-encoded body)
            .collect();
    
        let mut content_type: Option<String> = None;
        let mut content_length: Option<usize> = None;
    
        // RESTED doesn't use correct capitalization for headers sometimes
        for line in request_lines.iter().map(|s| s.to_lowercase()) {
            if let Some(s) = line.strip_prefix("content-type:") {
                content_type = Some(s.trim().to_string());
            }
    
            if let Some(s) = line.strip_prefix("content-length:") {
                content_length = Some(s.trim().parse().unwrap_or(0));
            }
        }
    
        if content_type.unwrap() == "application/x-www-form-urlencoded" && content_length.unwrap() > 0 {
            let buf = &mut vec![0; content_length.unwrap()];
            buf_reader.read_exact(buf).unwrap();
    
            let content = String::from_utf8_lossy(buf);
    
            println!("Received body: {content}");
        }
    }
    
    fn main() {
        let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
        for stream in listener.incoming() {
            handle_connection(&stream.unwrap());
        }
    }
    
    Received body: my_silly_request=void_angel