Skip to content

bug: Request(Dictionary) отправляет api_version под именем ApiVersion, узел его игнорирует и отвечает по v1 #97

Closed

Description

@Platonenkov

Что

IXrplClient.Request(Dictionary<string, object>) кладёт версию API под ключом nameof(ApiVersion), то есть буквально "ApiVersion":

if(!request.TryGetValue(nameof(ApiVersion), out var value)){
{
    request[nameof(ApiVersion)] = ApiVersion;
}}

Ключ уходит на провод как есть — словарь сериализуется без политики именования. rippled такого поля не знает, молча его игнорирует и обрабатывает запрос по версии по умолчанию. Настроенный на клиенте ApiVersion до узла не доходит никогда.

Доказательство

Один клиент, одна команда, одна и та же запрошенная версия. Дискриминатор — версия, которую узел заведомо не умеет: путь, который реально кладёт api_version на провод, обязан получить отказ.

подключились на api_version 2, затем client.ApiVersion = 99 (такой версии нет)

типизированный  ServerInfo()          : ОТКАЗ    -> invalid_API_version - Requested API version is higher than maximum supported (3)
нетипизированный Request(dictionary)  : УСПЕХ    -> версия не доставлена (ключи ответа: info, validated)

И то же на уровне провода, сырым WebSocket к wss://s2.ripple.com, подписка transactions:

без поля версии вообще  -> transaction (v1),  верхнеуровневый hash: нет
"api_version":2         -> tx_json (v2),      верхнеуровневый hash: да
"ApiVersion":2          -> transaction (v1),  верхнеуровневый hash: нет

Написание ApiVersion неотличимо от полного отсутствия поля. Типизированный путь при этом корректен: BaseRequest.ApiVersion помечен [JsonPropertyName("api_version")], а GRequest проставляет request.ApiVersion ??= ApiVersion с умолчанием 2.

Чем это плохо

Внутри одного клиента два пути дают разные версии протокола: client.AccountInfo(...) идёт по v2, а client.Request(new Dictionary { ["command"] = "account_info" }) — по v1, потому что узел не увидел версии и взял свою по умолчанию. Формы ответов между версиями отличаются, так что потребитель, смешивающий пути, получает несогласованные данные и никакого сигнала об этом.

Плюс на проводе в каждом таком запросе едет мусорное поле "ApiVersion".

Исправление и его риск

Само исправление тривиально — писать в словарь "api_version" и по нему же проверять, не задал ли вызывающий версию сам. Заодно стоит прибрать двойные скобки и неиспользуемый out var value.

Но правка меняет наблюдаемое поведение, и это надо решить осознанно. Сейчас все пользователи Request(Dictionary) фактически работают по v1. После починки они автоматически переедут на значение ApiVersion, а это по умолчанию 2 — то есть у них поменяются формы ответов на уже работающем коде, без единой строчки изменений с их стороны. Варианты, как это подать:

  • починить и описать в CHANGES.md как breaking с явным указанием, что затронут только нетипизированный путь;
  • починить, но для этого пути оставить умолчание 1, чтобы поведение не поменялось, и дать возможность задать версию явно;
  • не менять ключ, а бросать исключение, если в словаре встречается "ApiVersion" — то есть превратить тихую потерю в громкую ошибку.

Мне ближе первый вариант: сейчас поведение всё равно не то, которое заявлено, а расхождение между двумя путями одного клиента — худшая из возможных неожиданностей.

Как нашлось

Всплыло при проверке #96: там от версии API зависит, в каком свойстве лежит хеш транзакции, и по ходу выяснилось, что «SDK по умолчанию работает на v2» верно только для типизированного пути.

Activity

  1. Platonenkov commented on Aug 17, 2026

    @Platonenkov
    CollaboratorAuthor

    Исправлено в #99, коммит 73e83f7 на dev.

    Выбран первый из трёх вариантов, что обсуждались в задаче: ключ меняется на api_version, словарный путь начинает уважать настроенный ApiVersion, слом контракта описан в CHANGES.md явно — вместе с тем, как остаться на v1 (["api_version"] = 1 в словаре или ApiVersion на клиенте).

    Тесты читают то, что клиент реально кладёт на провод, через сервер-перехватчик запросов: поле, которое узел игнорирует, из ответа не видно — именно поэтому дефект не ловился ни одним существующим тестом. Написаны до правки и падали все три.

    Главный риск правки — что словарный путь в тестах и демо поедет по v2 — проверен отдельно: интеграционные на standalone-ноде 265 / 265, и повторно в очереди мержа на результате слияния. Ни одна команда в v2 не ответила иначе, чем ожидают модели.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions