# Глава 1.8. Настройка приложения Сервер из прошлых глав умеет принимать запросы и отвечать на них, но для реального приложения ему не хватает нескольких вещей: настроек, которые можно менять без пересборки, доступа из браузерного фронтенда и понятных сообщений об ошибках компиляции. В этой главе разберём их, а также поговорим о выборе асинхронного рантайма и о том, как фичи и профили сборки влияют на компиляцию проекта. ## Асинхронный рантайм Асинхронный рантайм — это крейт, который выполняет асинхронный код: распределяет задачи по потокам, работает с сетью, таймерами и т.д. Стандартная библиотека Rust не содержит асинхронного рантайма, а даёт только инструменты для его написания, поэтому рантайм подключают к проекту отдельным крейтом. Фактическим стандартом в экосистеме Rust является Tokio. Другие рантаймы тоже существуют, но решают другие задачи: | Рантайм | Когда стоит взять | | --------------- | ------------------------------------------------------------------------------------------------------------ | | smol | Небольшие утилиты и библиотеки, которым важен минимум зависимостей | | Embassy | Микроконтроллеры без операционной системы | | Glommio, Monoio | Экстремальная нагрузка на ввод-вывод под Linux, например прокси или хранилища, где важна каждая микросекунда | Glommio и Monoio используют механизм `io_uring` ядра Linux. Выигрыш от этого заметен только на очень высоких нагрузках, а экосистема у них своя и небольшая: Axum и sqlx с ними не работают. В старых статьях можно встретить ещё и async-std, но его разработка [прекращена](https://github.com/async-rs/async-std), а авторы рекомендуют переходить на smol. Axum построен на Tokio, поэтому в этом курсе выбирать рантайм не придётся. Альтернативы стоит рассматривать для проектов, где Axum не используется. ## Фичи Cargo Многие крейты разбиты на отключаемые части — фичи (features). Фича включает дополнительную функциональность, а часто и дополнительные зависимости. Например, у Axum поддержка WebSocket вынесена в фичу `ws`, а макросы — в фичу `macros`. Часть фич включена по умолчанию. Фичи указываются в `Cargo.toml`: ```toml [dependencies] axum = { version = "0.8", features = ["macros"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` Список доступных фич можно посмотреть несколькими способами: - Команда `cargo add` выводит его при добавлении зависимости. Плюсом отмечены включённые фичи, минусом — выключенные. - На docs.rs у каждого крейта есть страница с фичами, например [docs.rs/crate/axum/latest/features](https://docs.rs/crate/axum/latest/features). - В разделе `[features]` файла `Cargo.toml` самого крейта. Фичи напрямую влияют на время компиляции: каждая включённая фича добавляет код, а иногда и целые зависимости, которые тоже придётся собрать. Например, в примерах из интернета часто встречается `tokio = { features = ["full"] }`. Это удобно, но в сборку попадёт всё, включая работу с файлами, процессами и сигналами, даже если приложению это не нужно. Лучше включать только то, что действительно используется. ## Профили сборки Cargo собирает проект в одном из двух основных профилей: `dev` и `release`. Команды `cargo build` и `cargo run` по умолчанию используют профиль `dev`: компиляция идёт быстрее, но код почти не оптимизируется, а в программу добавляются дополнительные проверки, например паника при переполнении целых чисел. С флагом `--release` используется профиль `release`: сборка занимает заметно больше времени, зато программа работает в разы быстрее. Результаты сборки в профиле `dev` попадают в каталог `target/debug`, а в профиле `release` — в `target/release`. ```bash cargo run # сборка для разработки cargo run --release # релизная сборка ``` Поэтому при разработке пользуются обычным `cargo run`, а для боевого сервера и для замеров производительности собирают проект с флагом `--release`. Сравнивать скорость сервера по сборке для разработки бессмысленно: она может отличаться от релизной в десятки раз. ## Переменные окружения Адрес сервера, строка подключения к базе данных, ключи внешних сервисов различаются на компьютере разработчика и на боевом сервере. Хранить их в коде неудобно, а секреты ещё и небезопасно. Поэтому настройки обычно передают через переменные окружения и читают один раз — при старте приложения: ```rust use anyhow::Context; #[derive(Clone)] struct Config { host: String, port: u16, frontend_url: String, } impl Config { fn from_env() -> anyhow::Result<Self> { let host = std::env::var("HOST") .unwrap_or_else(|_| "127.0.0.1".to_string()); let port = std::env::var("PORT") .unwrap_or_else(|_| "3000".to_string()) .parse() .context("PORT должен быть числом от 0 до 65535")?; let frontend_url = std::env::var("FRONTEND_URL") .context("не задана переменная FRONTEND_URL")?; Ok(Self { host, port, frontend_url }) } } ``` У необязательных настроек есть значения по умолчанию, а без обязательных приложение не запустится: ```text Error: не задана переменная FRONTEND_URL Caused by: environment variable not found ``` Готовую структуру `Config` можно положить в состояние приложения, и хендлеры получат доступ к настройкам. Задавать переменные в терминале при каждом запуске неудобно, поэтому при разработке их хранят в файле `.env` в корне проекта: ```text FRONTEND_URL=http://localhost:5173 PORT=3000 ``` Прочитать его поможет крейт `dotenvy`: ```bash cargo add dotenvy ``` ```rust #[tokio::main] async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); let config = Config::from_env()?; let addr = format!("{}:{}", config.host, config.port); let listener = TcpListener::bind(&addr).await?; // ... } ``` Функция `dotenv` загружает переменные из файла `.env`, не перезаписывая уже заданные. Вызов `ok()` отбрасывает ошибку, если файла нет: на боевом сервере переменные обычно задаются иначе, и файла `.env` там нет. Файл `.env` может содержать пароли и ключи, поэтому его добавляют в `.gitignore`. В репозиторий вместо него кладут `.env.example` с примерами значений. ## CORS Если к API обращается фронтенд из браузера, в дело вступает политика одинакового источника (same-origin policy). Источник — это сочетание протокола, домена и порта. Фронтенд на `http://localhost:5173` и API на `http://127.0.0.1:3000` — разные источники, и браузер по умолчанию не даст странице прочитать ответ от чужого сервера. CORS (Cross-Origin Resource Sharing) — механизм, которым сервер сообщает браузеру, каким источникам разрешено читать его ответы. Перед многими запросами, например перед `POST` с JSON, браузер сначала отправляет предварительный запрос с методом `OPTIONS` и проверяет, разрешает ли сервер такой запрос. В Axum CORS настраивается слоем из крейта `tower-http`: ```bash cargo add tower-http --features cors ``` ```rust use anyhow::Context; use axum::http::{header, HeaderValue, Method}; use tower_http::cors::CorsLayer; let origin: HeaderValue = config .frontend_url .parse() .context("FRONTEND_URL содержит недопустимые символы")?; let cors = CorsLayer::new() .allow_origin(origin) .allow_methods([Method::GET, Method::POST]) .allow_headers([header::CONTENT_TYPE]); let router = Router::new() .route("/", get(hello)) .layer(cors); ``` Слой разрешает запросы с адреса фронтенда, методы `GET` и `POST` и заголовок `content-type`. Адрес фронтенда берётся из настроек, потому что на компьютере разработчика и на боевом сервере он разный. Ответ на предварительный запрос выглядит так: ```text $ curl -i -X OPTIONS -H 'Origin: http://localhost:5173' \ -H 'Access-Control-Request-Method: POST' http://127.0.0.1:3000/ HTTP/1.1 200 OK access-control-allow-methods: GET,POST access-control-allow-headers: content-type access-control-allow-origin: http://localhost:5173 ``` Важно понимать, что CORS защищает пользователей браузера, а не сам сервер. Сервер не отклоняет запросы с чужих адресов: он лишь сообщает браузеру разрешённый источник, а блокирует ответ сам браузер. Запросы через curl или от другого сервера CORS не ограничивает, поэтому доступ к данным нужно защищать аутентификацией. О ней поговорим в третьем модуле. Для локальной разработки есть `CorsLayer::permissive()`, который разрешает всё. На боевом сервере его использовать не стоит. ## Ошибки компиляции хендлеров Если хендлер не компилируется, компилятор обычно выдаёт общую ошибку: функция не реализует трейт `Handler`. Например, если поставить экстрактор `Json` перед `Path`: ```text error[E0277]: the trait bound `fn(Json<UpdateUser>, Path<u64>) -> ... {update_user}: Handler<_, _>` is not satisfied ``` Понять, какой именно аргумент мешает, помогает атрибут `#[axum::debug_handler]` из фичи `macros`. С ним компилятор указывает на конкретную причину: ```rust #[axum::debug_handler] async fn update_user( Json(user): Json<UpdateUser>, Path(id): Path<u64>, ) -> String { // ... } ``` ```text error: `Json<_>` consumes the request body and thus must be the last argument to the handler function ``` Атрибут нужен только для поиска ошибки: после исправления его можно убрать. На этом введение в Axum закончено: мы умеем описывать маршруты, принимать и возвращать данные, хранить состояние, обрабатывать ошибки и настраивать приложение. Пока все данные живут в памяти и пропадают при перезапуске. В следующем модуле подключим базу данных PostgreSQL.

Следующие главы находятся в разработке

Наша группа в telegram. Задавайте вопросы и общайтесь.
Задонатить. Поддержите развитие площадки.