Files
mtusi2026lab6/contracts

Семантика 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 — 1100 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. Это учебный выбор пользователя: любой клиент может выдать себя за другого.

Lab27: POST /auth/sessions принимает только Authorization: Basic <base64(username:password)>, без JSON body. Username ограничен ASCII; пароль кодируется UTF-8, 12128 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, не бинарное содержимое.