Семантика API
Этот текст дополняет openapi.json. Обязательный API соответствует уровню 3 текущей лабораторной. Факультативный crop или rate limiting не становятся обязательными только из-за наличия допустимого имени варианта или кода ошибки в схеме.
Формат и ошибки
- JSON — UTF-8,
Content-Type: application/json. UUID — строка; время — RFC 3339 UTC сZ, назначается сервером. Часы не считаются уникальным ID. - Каждый ответ API, включая ошибки и health, содержит непустой
X-Request-Id. Тело ошибки:{"error":{"code":"invalid_request","message":"...","request_id":"..."}};request_idсовпадает с заголовком. Сообщение ошибки не раскрывает stack trace, SQL, credentials или наличие чужого объекта. - Обязательные строки не состоят только из пробельных символов.
usernameстрого[a-z0-9_]{3,32}, уникален;display_name— 1–100 Unicode code points. JSON с лишними полями или неверными типами —400; malformed JSON также400. 400 invalid_request— неверные поля, UUID, cursor, limit;401 unauthorized— нет действительной идентификации/сессии;404 not_found— объекта нет или он чужой;409 conflict— username занят, повторный ключ с другим сообщением, объект ещё не готов;413 payload_too_large;415 unsupported_media_type;429 rate_limited;503 unavailable. Неожиданное исключение —500 internal_errorбез внутренних деталей.- Сначала аутентификация, затем доступ к конкретному ресурсу. Любой аутентифицированный участник чата может читать его историю и вложения и писать в него; посторонний получает
404. Списки содержат только доступные объекты. Не используйте403для раскрытия существования чужого чата. GET /health/liveпроверяет только жизнь процесса, возвращает200 {"status":"ok"}.GET /health/readyвозвращает 200, если приложение может безопасно обслуживать обязательный синхронный путь; иначе 503 в общем формате ошибки. Какие зависимости критичны, зафиксируйте в отчёте; недоступность Grafana не должна останавливать API.
Пользователи, чаты и сообщения
POST /usersпубличен; возвращает201 User. Пароль/его hash и сессионные данные никогда не входят в User.POST /chatsпринимает title и явный список существующихmember_ids, включая вызывающего. Дубликаты, неизвестные участники и отсутствие вызывающего — 400. Чат создаётся атомарно. GET списка чатов сортируется по(created_at, id)по возрастанию.- Отправитель сообщения выводится из текущей идентификации, поле
sender_idв запросе не принимается. Текст — до 4000 Unicode code points. Нельзя отправить пустой/пробельный текст без вложений. - История использует keyset cursor:
(created_at, id), по возрастанию;limit— целое 1–100, по умолчанию 50. Ответ{"items":[...],"next_cursor":null}. Непустой cursor — непрозрачная для клиента строка; последний элемент предыдущей страницы не повторяется. Cursor привязан к ресурсу/запросу: от другого чата — 400. Изменятьlimitпри продолжении можно. next_cursorравен null, если на момент чтения следующей записи нет. Гарантируется отсутствие повторов при обходе неизменной истории; snapshot изоляция между несколькими HTTP-запросами не требуется. При конкурентных вставках опишите выбранную семантику и ограничения часов.- Подтверждённое сообщение видно следующему чтению через любую API-реплику. Допускается кэш, но он не должен возвращать устаревшую историю после успешной записи. Удаление и изменение участников в обязательный API не входят.
Идентификация и сессии
Lab1: защищённые endpoint требуют X-User-Id: <UUID существующего пользователя>; отсутствие/неизвестный ID — 401. Это учебный выбор пользователя: любой клиент может выдать себя за другого.
Lab2–7: POST /auth/sessions принимает только Authorization: Basic <base64(username:password)>, без JSON body. Username ограничен ASCII; пароль кодируется UTF-8, 12–128 code points, двоеточие в пароле допустимо. Неверные credentials — одинаковый 401 и WWW-Authenticate: Basic realm="messenger", charset="UTF-8".
Вход возвращает 201 {"token":"...","token_type":"Bearer","expires_at":"...Z","user":{...}}. Токен — непрозрачное случайное значение, не username/UUID и не обязательный JWT; энтропия не менее 256 бит. Храните серверную сессию с expiry; TTL задаётся SESSION_TTL_SECONDS, по умолчанию 3600 (в тесте истечения можно 2). В обычном smoke TTL должен быть не менее 300.
Все защищённые endpoint принимают только Authorization: Bearer <token>. Истёкший/отозванный/неизвестный токен — 401 с WWW-Authenticate: Bearer realm="messenger". Basic на бизнес-endpoint — 401. X-User-Id с lab2 игнорируется: он не меняет владельца сессии и сам по себе не даёт доступ. DELETE /auth/sessions/current отзывает текущий токен и возвращает 204 без тела; повтор с отозванным токеном — 401.
Пользователи, чаты, сообщения и ещё действительные сессии переживают перезапуск приложения и используемых хранилищ с сохранёнными volumes. TTL не продлевается из-за рестарта. Пароли уже с lab2 хешируются библиотечным password KDF (Argon2id, scrypt или bcrypt с учётом ограничений библиотеки), не SHA-256 и не plaintext. Если bcrypt не поддерживает весь диапазон UTF-8 паролей без усечения, выберите другую библиотечную схему. В lab7 обязателен Argon2id.
Basic — это кодирование, не шифрование. HTTP lab1–6 допускается только для локального изолированного стенда с синтетическими данными; для удалённой демонстрации нужен TLS. Lab7 использует HTTPS обязательно. См. RFC 7617.
Балансировка и повторная отправка (с lab3)
- Каждый ответ, созданный API, содержит
X-Instance-Id; значение стабильно для жизни процесса. Ошибки соединения, созданные самим LB, могут не иметь этого заголовка. В lab7 используйте случайный alias реплики без раскрытия hostname/IP. - Для POST сообщения обязателен
Idempotency-Key(ASCII[A-Za-z0-9._:-]{1,128}). Нет/неверный ключ — 400. Область уникальности: текущий пользователь + HTTP-метод + путь чата + ключ. - При повторе того же валидного JSON в течение 24 часов сервер возвращает тот же код 201 и то же Message. Порядок ключей JSON и отсутствующий
attachment_idsвместо[]семантически равны. Другой body при том же ключе — 409. Разные пользователи/чаты могут использовать одинаковый ключ независимо. - Одновременные повторы через разные реплики создают ровно одно сообщение. Повтор снова проверяет действительность сессии и доступ. Транзиентные 5xx не фиксируются как окончательный результат; побочный эффект и запись результата должны быть атомарны.
- Ключ обязателен только для сообщений. Самостоятельный retry загрузки файла или создания чата может создать ещё один объект; не добавляйте прозрачные повторы этих POST без расширения контракта.
Вложения (с lab4)
POST /chats/{chat_id}/attachments?filename=photo.png: сырые bytes одного PNG/JPEG;Content-Type: image/pngилиimage/jpeg. Не multipart. Максимум 10 MiB = 10 485 760 байт. MIME проверяется по содержимому; заведомо повреждённые/чужие форматы — 415, превышение bytes — 413. Не более 40 миллионов декодированных пикселей (превышение — 413). Устанавливайте лимит до полной распаковки изображения.filename— отображаемое имя (1–255 code points). Не используйте его как путь/ключ S3. Имя с/,\, NUL или управляющими символами — 400. Bucket приватен; object key генерирует сервер. Оригинал возвращается побайтно, его lowercase SHA-256 и размер соответствуют исходному файлу.- Lab4 возвращает
201 Attachmentсоstatus=ready,variants=[],error_code=nullтолько после сохранения оригинала и метаданных. Lab5–7 возвращают202 Attachmentсо снимкомstatus=queued; готовность определяется последующим GET метаданных. Быстрый worker может закончить до первого GET — это нормально. - Файл заранее привязан к чату. В POST сообщения добавляется
attachment_ids(0–10 уникальных ID, по умолчанию []). Можно отправитьtext=""с хотя бы одним вложением. Все вложения должны принадлежать этому чату; чужое/неизвестное — 404. Любой участник может ссылаться на доступное вложение чата. Failed-вложение прикреплять нельзя (409); queued/processing с lab5 можно. GET /attachments/{id}и/contentдоступны только участникам чата. Download возвращает байты с истинным Content-Type, через авторизованный API, без redirect в обязательном контракте. Приватный bucket сам по себе не заменяет эту проверку.variantпо умолчанию original. Для queued/processing content — 409; для failed — 409. Отсутствующий вариант ready-объекта — 404.- В ready-метаданных
error_code=null. В failed — краткий машинный код (напримерinvalid_image,processing_failed,retry_exhausted), без stack trace.size_bytes,sha256,content_typeвсегда описывают оригинал; у вариантов отдельные поля. Публичный API не раскрывает ключи бакета, внутренние адреса и credentials. - Прямой upload/download по временной подписи — факультативное расширение. Его дополняют ограничение срока/размера, уникальный ключ и подтверждение реально загруженного объекта; он не заменяет стандартный endpoint. Особенности подписанных URL: S3.
Фоновая обработка (с lab5)
До 202 все байты приняты в устойчивый staging, а намерение обработать файл надёжно сохранено. HTTP-передача клиента не исчезает и не становится мгновенной. Staging — приватный S3-prefix либо общий persistent volume, доступный worker; локальная память/API ephemeral disk не подходит. Worker асинхронно переносит/публикует оригинал в конечный S3-prefix и создаёт варианты; если staging уже в S3, опишите эту границу честно.
Обязательный thumbnail: JPEG, изображение вписано в 256×256 с сохранением пропорций, без увеличения маленьких исходников, EXIF orientation применяется; alpha компонуется на белый. Размеры округляются вниз, минимум 1 px. Fixture 320×200 должен дать 256×160. Статус ready ставится после сохранения оригинала и thumbnail. Crop на 4: JPEG, центрированный квадрат 128×128 (для маленьких исходников допускается увеличение). Optimized на 4: WebP, вписан в 1280×1280 без увеличения, параметры качества и удаления EXIF документируются; не обещайте уменьшение каждого маленького файла.
Допустимые состояния и события описаны в jobs.schema.json и attachment-states.md. Доставка — at least once; обработка идемпотентна, повторы не размножают варианты. Финальные ошибки наблюдаемы, бесконечный немой retry недопустим. Обязательный smoke ждёт ready до 60 секунд; это бюджет локального smoke для одного файла при свободном worker, не универсальный production SLA. В очередь идут ссылки/ID, не бинарное содержимое.