Я пытаюсь создать простой CRUD API на Rust, следуя этому руководству, и сейчас нахожусь на этапе тестирования своих усилий. К сожалению, я столкнулся с ошибкой при использовании запроса POST, который, по сути, является общим INTERNAL_SERVER_ERROR, который я написал.
Я тестирую API через Postman, с методом, установленным на POST, URL-адресом, установленным на localhost:8080/logs, и телом, установленным на необработанный JSON со следующим содержимым:
{
"app_id": "ROBA",
"log_title": "Meaningful log title",
"app_title": "Robot Battles",
"log_content": "Error at this line with this module, etc."
}
Вот мой docker-compose.yml:
версия: '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: {}
А вот сокращенная версия моего 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())
}
И наконец, мой 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"]
Попытки добавить в базу данных через POST просто приводят к ошибке, и пока я не совсем уверен, что делаю не так. Любая помощь приветствуется.
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
@Jmb, повторно добавил ошибку в виде текста.
Попробуйте распечатать ошибки, а не отбрасывать их. Например. вместо сопоставления _ => { … } попробуйте что-то вроде: (a, c) => { if let Err (e) = a { println!("Error: app_logger: {e:?}"); } if let Err (e) = c { println!("Error: client: {e:?}"); } … }





Как предложил @Jmb выше, мне пришлось написать больше кода, чтобы лучше понять проблему, поэтому я добавил следующее в match case в 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())
}
Это подсказало мне суть проблемы; их было несколько, но в первую очередь это терпит неудачу сразу же при попытке получить тело запроса и попытаться превратить его в AppLog, потому что структура AppLog ожидает log_datetime, чего я не делал (и не должен) передать как часть тела запроса.
Поэтому я изменил структуру AppLog на следующую:
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
}
Далее последовал ряд вопросов:
Cargo.toml, чтобы включить serde в файл Chronic. Исходил из этого:chrono = 0.4
К этому:
chrono = {version = "0.4", features = ["serde"]}
AppLog. Гораздо позже выяснилось, что мне также нужно изменить зависимость postgres в Cargo.toml, исходя из этого:postgres = 0.4
К этому:
postgres = {version = "0.19.2", features = ["with-chrono-0_4"]}
utc.to_string(). Я все еще работаю над этим, но на данный момент я удалил этот параметр и поле из инструкции INSERT в качестве обходного пути, поскольку таблица все равно вставляет now() по умолчанию.
моя первая мысль — сеть докеров, попробуйте использовать
exposeвместоportsв вашем docker-compose.yaml