Search code examples
postgresqldockerrustdocker-compose

Rust API in docker with Postgresql DB, POST request error


I'm trying to build a simple Rust CRUD API, following along this tutorial, and I'm at the part where I should be testing my efforts. Unfortunately, I have been encountering an error with using the POST request, which is basically the generic INTERNAL_SERVER_ERROR I coded in.

I'm testing the API via Postman, and with the method set to POST, the url set to localhost:8080/logs, and the body set to raw JSON with the following contents:

{
    "app_id": "ROBA",
    "log_title": "Meaningful log title",
    "app_title": "Robot Battles",
    "log_content": "Error at this line with this module, etc."
}

Here's my docker-compose.yml:

version: '3.9'

services:
  rust_db:
    container_name: rust_db
    image: postgres
    environment:
      POSTGRES_DB: apploggerstorage
      POSTGRES_USER: apploggeruser
      POSTGRES_PASSWORD: apploggerpw1234
    ports:
      - '5432:5432'
    volumes:
      - pgdata:/var/lib/postgresql/data
  rust_app_logger:
    container_name: rust_app_logger
    image: lanceguinto/rust_app_logger:1.0.0
    build:
      context: .
      dockerfile: Dockerfile
      args:
        DATABASE_URL: postgres://apploggeruser:apploggerpw1234@rust_db:5432/apploggerstorage
    ports:
      - '8080:8080'
    depends_on:
      - rust_db

volumes:
  pgdata: {}

And here's a shortened version of my main.rs:

use postgres::{Client, NoTls};
use postgres::Error as PostgresError;
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::env;
use chrono::prelude::*;

#[macro_use]
extern crate serde_derive;

#[derive(Serialize, Deserialize)]
struct AppLog {
    id: Option<i32>, // Option because ID is not provided by app but by database
    app_id: String,
    log_title: String, 
    app_title: String,
    log_content: String,
    log_datetime: String,
}

const DB_URL: &str = env!("DATABASE_URL");

const OK_RESPONSE: &str = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
const NOT_FOUND: &str = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
const INTERNAL_SERVER_ERROR: &str = "HTTP/1.1 500 INTERNAL SERVER ERROR\r\n\r\n";

fn main() {
    if let Err(e) = set_database() {
        print!("Error: {}", e);
        return;
    }

    let listener = TcpListener::bind(format!("0.0.0.0:8080")).unwrap();
    println!("Server started at port 8080.");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                handle_client(stream);
            }
            Err(e) => {
                println!("Error: {}", e);
            }
        }
    }
}

fn handle_client(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    let mut request = String::new();

    match stream.read(&mut buffer) {
        Ok(size) => {
            request.push_str(String::from_utf8_lossy(&buffer[..size]).as_ref());
            let (status_line, content) = match &*request {
                r if r.starts_with("POST /logs") => handle_post_request(r),
                _ => (NOT_FOUND.to_string(), "404 Not Found".to_string()),
            };

            stream.write_all(format!("{}{}", status_line, content).as_bytes()).unwrap();
        }
        Err(e) => {
            println!("Error: {}", e);
        }
    }
}

fn handle_post_request(request: &str) -> (String, String) {
    match (get_applog_request_body(&request), Client::connect(DB_URL, NoTls)) {
        (Ok(app_logger), Ok(mut client)) => {
            let utc: DateTime<Utc> = Utc::now();
            client.execute(
            "INSERT INTO logs (appId, logTitle, appTitle, logContent, logDateTime) VALUES ($1, $2, $3, $4, $5)", 
            &[&app_logger.app_id, &app_logger.log_title, &app_logger.app_title, &app_logger.log_content, &utc.to_string()]
            ).unwrap();

            (OK_RESPONSE.to_string(), "Log posted".to_string())
        }

        _ => {
            // This is what shows up
            println!("Error: {}", INTERNAL_SERVER_ERROR);
            (INTERNAL_SERVER_ERROR.to_string(), "Error".to_string())
        }
    }
}

fn set_database() -> Result<(), PostgresError> {
    let client = Client::connect(DB_URL, NoTls);

    client?.batch_execute(
        "CREATE TABLE IF NOT EXISTS logs (
            id SERIAL PRIMARY KEY, 
            appId TEXT NOT NULL, 
            logTitle TEXT NOT NULL, 
            appTitle TEXT, 
            logContent TEXT, 
            logDateTime TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
        )"
    )?;
    Ok(())
}

fn get_applog_request_body(request: &str) -> Result<AppLog, serde_json::Error> {
    serde_json::from_str(request.split("\r\n\r\n").last().unwrap_or_default())
}

Finally, my Dockerfile:

FROM rust:1.69-buster as builder
WORKDIR /app
# Build Args
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
COPY . .
RUN cargo build --release
FROM debian:buster-slim
WORKDIR /usr/local/bin
COPY --from=builder /app/target/release/app-logger .
CMD ["./app-logger"]

Attempts to add to the database via POST just results in the error, and thus far I'm not entirely sure what I'm doing wrong. Any help would be much appreciated.

rust_app_logger    | Get App Log Request Body: POST /logs HTTP/1.1
rust_app_logger    | Content-Type: application/json
rust_app_logger    | User-Agent: PostmanRuntime/7.28.2
rust_app_logger    | Accept: */*
rust_app_logger    | Cache-Control: no-cache
rust_app_logger    | Postman-Token: f148f831-3bf5-4541-8924-75ae005639ad
rust_app_logger    | Host: localhost:8080
rust_app_logger    | Accept-Encoding: gzip, deflate, br
rust_app_logger    | Connection: keep-alive
rust_app_logger    | Content-Length: 168
rust_app_logger    |
rust_app_logger    | {
rust_app_logger    |     "app_id": "ROBA",
rust_app_logger    |     "log_title": "Meaningful log title",
rust_app_logger    |     "app_title": "Robot Battles",
rust_app_logger    |     "log_content": "Error at this line with this module, etc."
rust_app_logger    | }
rust_app_logger    | Error: HTTP/1.1 500 INTERNAL SERVER ERROR

Solution

  • As @Jmb suggested above, I had to write more code to get a better understanding of the issue, so I added the following into the match case in fn handle_post_request:

    (Ok(_app_logger), Err(e)) => {
        println!("Error with the client connection: {}", e);
        (INTERNAL_SERVER_ERROR.to_string(), e.to_string())
    }
    
    (Err(e), Ok(_client)) => {
        println!("Error with getting applog request body: {}", e);
        (INTERNAL_SERVER_ERROR.to_string(),  e.to_string())
    }
    

    This clued me in to the issue; there were multiple, but it fails first and foremost right out of the bat when trying to retrieve the request body and trying to turn it into an AppLog, because the struct AppLog expects a log_datetime, which I didn't (and shouldn't) pass as part of the request body.

    Therefore, I changed the struct AppLog to the following:

    struct AppLog {
        id: Option<i32>, // Option because ID is not provided by app but by database
        app_id: String,
        log_title: String, 
        app_title: String,
        log_content: String,
        log_datetime: Option<DateTime<Utc>>, // Option because log date time is not provided by request but by API / database
    }
    

    What followed then was a series of issues:

    1. Something about DateTime not being supported by Deserialize or Serialize; I figured out much later that I also needed to modify the Cargo.toml file, to include serde in chrono. Went from this:
    chrono = 0.4
    

    To this:

    chrono = {version = "0.4", features = ["serde"]}
    
    1. My API also has GET requests, but those had issues with retrieving logs from the DB and outputting them as an array of struct AppLog. Figured out much later that I also needed to modify the postgres dependency in Cargo.toml, from this:
    postgres = 0.4
    

    To this:

    postgres = {version = "0.19.2", features = ["with-chrono-0_4"]}
    
    1. A third issue is that the INSERT statement still doesn't work when inserting the utc.to_string(). I'm still working on that, but for now I've removed that parameter and field from the INSERT statement as a workaround, since the table does insert now() as default anyway.