# Глава 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. Задавайте вопросы и общайтесь.
Задонатить. Поддержите развитие площадки.