---
name: vsub-xray-json-raw-not-base64
description: "vsub-tg-bot: подписки формата xray_json отдавать СЫРЫМ JSON (raw_path), не base64 — иначе Happ показывает пустой список стран"
metadata: 
  node_type: memory
  type: project
  originSessionId: ca9d2fe6-c078-4428-b2e2-b28e031e1440
  modified: 2026-08-23T00:07:51.390Z
---

В `~/Services/vsub-tg-bot` подписка бывает двух форматов: `vless_links` (стандарт — base64 от списка `vless://`) и `xray_json` (JSON-массив полных Xray-конфигов). Happ декодирует base64 ТОЛЬКО для vless_links; для `Content-Type: application/json` он ждёт сырой JSON. Если xray_json отдать в base64-обёртке — подписка «добавляется, но список стран пустой» (не парсится).

Единственный источник правды о формате и о том, чьи байты сохранять, — `bot/snapshot.py::store_artifact(snapshot_dir, raw_path, compact_path)`: если `raw_path` кончается на `.json` → формат `xray_json`, сохраняем raw_path (сырой JSON); иначе `vless_links`, сохраняем compact_path (base64). `compact_path` у vsub_service ВСЕГДА зовётся `subscription-base64.txt` независимо от формата — определять формат по имени compact нельзя.

**Why:** баг сидел в 3 вызывающих местах (handlers.py `_write_snapshot_entry`, rebuild.py, storage.py `_latest_hand_build`) — каждое само решало source/format и путало. Свёл решение в одну функцию. Чинил 2026-08-23 после жалобы «vpn добавляется, но стран нет».

**How to apply:** не отдавай compact_path (base64) для xray_json. Проверка после правок: `curl https://sub.cebceh.com/sub/<token>` → тело должно парситься как JSON напрямую (не через base64), Content-Type `application/json`. Миграция битых токенов — пересчёт artifact из `subscriptions.raw_path`. См. [[vsub-vpn-bot-admin]].
