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
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:
Cargo.toml
file, to include serde
in chrono. Went from this:chrono = 0.4
To this:
chrono = {version = "0.4", features = ["serde"]}
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"]}
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.