Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,63 @@

### Added

- **REST API 3.0 — по адресу вызова.** Чтобы вызывать методы новой версии,
достаточно передать адрес с сегментом `/rest/api/`. Опции для этого нет и не
будет:

```go
client := b24.NewClient("https://portal.bitrix24.ru/rest/api/1/токен/")
res, err := client.Core().Call(ctx, "tasks.task.list", b24.Params{
"select": []string{"id", "title"},
"pagination": b24.Params{"limit": 20, "page": 1},
})
```

Версия выводится из адреса, потому что адрес её и так задаёт: без `/api/`
портал выполнит метод старой версии или ответит «метод не найден». Второй
источник истины можно рассогласовать с первым — сказать «версия 3» и забыть
`/api/`, — и тогда каждый вызов уходил бы в v1, а разбирался бы по правилам v3.

Мешало ровно одно: SDK безусловно дописывал к методу `.json`, а v3 такой суффикс
не принимает — `…/rest/api/1/токен/documentation` отвечает 200, а
`documentation.json` — 404 «Метод `documentation.json` не найден». Теперь суффикс
дописывается только для v1, и **поведение v1 не изменилось ни в чём** — на это
есть отдельные регрессионные тесты.

Конверт **успешного** ответа у v3 тот же, что у v1, поэтому `CallResult`,
`Result`, `Kind`, `Unwrap`, `IsEmpty`, `ID` работают без изменений.

- **Разбор ошибок REST 3.0.** У v3 код и текст лежат во вложенном объекте
(`{"error":{"code":…,"message":…}}`), а не плоско, — `*APIError` заполняется
из обеих форм, и `errors.Is`, `CodeOf` и вся таксономия продолжают работать.

Форму выбирает **тип поля `error`, а не версия адреса**: адрес v3 отвечает
обеими. Замер: `…/rest/api/…/tasks.task.get` с `{"id":"abc"}` отвечает HTTP 500
и **плоским** телом `{"error":"INTERNAL_SERVER_ERROR",…}` — это докладывает
шлюз REST, стоящий перед контроллером v3. Разбор по версии потерял бы код всех
таких ошибок, включая `QUERY_LIMIT_EXCEEDED`, на котором держатся повторы.

Коды у версий разные, и SDK сводит к старому **один** — тот, у которого на
обеих версиях одно и то же значение: `errors.Is(err, ErrMethodNotFound)`
срабатывает и на `BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION`. Остальные
не сводятся намеренно: `…_ACCESSDENIEDEXCEPTION` похож на `ACCESS_DENIED`, но
v3 отвечает им и на неверный токен вебхука, где v1 отвечает
`INVALID_CREDENTIALS`, — один код v3 покрывает два кода v1, и сведение
заставило бы ветку «права не те, авторизация в порядке» срабатывать на
протухшей авторизации. Для них — `ErrV3Validation`, `ErrV3EntityNotFound`,
`ErrV3AccessDenied` и константы `CodeV3*`.

Приставка `BITRIX_REST_V3_EXCEPTION_` **не универсальна**, выводить код из неё
нельзя: `crm.deal.timeline.activity.email.list` на плохой `id` отвечает
`CRM_EMAIL_INVALID_REQUEST` в том же конверте. Поэтому `CodeOf` возвращает код
**как он пришёл**, без перевода: он идёт в лог, и чужой код там отправил бы
читателя искать строку, которой портал не присылал.

- **`APIError.Validation`** — поля, из-за которых v3 отклонил запрос. Код и текст
у всех таких ошибок одинаково общие («Ошибка при валидации объекта запроса»),
так что список полей — единственная часть, говорящая, что именно не так.
У v1 аналога нет, там он пустой.

- **`Result` с `Kind()`.** Одно и то же поле Битрикс24 отвечает разной формой, и
какой именно — зависит от **данных**, а не от метода: свойство товара с одним
значением приходит объектом, с несколькими — массивом тех же объектов; а
Expand Down Expand Up @@ -158,6 +215,61 @@
из `crm.deal.update` и `crm.deal.get` — два нуля и ошибка, называющая обе
команды и цитирующая их ответы. Всё созданное удалено этими же
идентификаторами, отсутствие проверено чтением.
### Changed

- **`Pages`/`Scan` и `CallBatch`/`CallBatchChunked` на адресе v3 отказываются
работать** — `ErrV3WalkUnsupported` и `ErrV3BatchUnsupported`, до отправки
запроса.

У v3 нет курсора: `start` игнорируется, `next` и `total` в ответе
отсутствуют, страница задаётся параметром `pagination` (`page`, `limit`,
`offset`). Без отказа обход выглядел бы работающим: на живом портале `Pages`
по `tasks.task.list` прочитал первую страницу, не увидел `next` и отчитался о
**завершённом** обходе с `Err() == nil` — 2 строки из 423. Частичная выгрузка,
выглядящая как полная, — то, чего у обхода быть не должно.

Метод `batch` у v3 есть, но это другой протокол: команды кладутся в **корень**
тела как `{"method": …, "query": {…}}`, ответ — **массив** в порядке отправки
(ключи команд отбрасываются), а первая упавшая команда обрывает весь запрос
вместо `result_error`. Поэтому `Batch`, `Ref`, `Halt` и `BatchResult` —
протокол v1 — отображать не на что, а частичной ошибки, ради которой
существует `BatchError`, там не бывает. Отказ заменяет собой ответ портала на
тело v1-батча: `BITRIX_REST_V3_EXCEPTION_INVALIDSELECTEXCEPTION`, «Не удается
распознать выражение select» — сообщение про `select` на запрос, где его нет.

Оба sentinel'а называют, чем пользоваться вместо них: `Core.Call` с
`pagination` и `Core.Call` с `batch`.

На v1 не влияет: версия выводится из адреса, и обход с батчем на `/rest/`
работают как раньше.

### Проверено на живом портале — REST 3.0

Всё ниже — ответы облачного портала, снятые вызовами самого SDK.

- `.json`: `…/rest/api/1/токен/documentation` → 200,
`…/documentation.json` → 404 «Метод `documentation.json` не найден».
- Конверт успеха совпадает с v1: `humanresources.employee.count` →
`{"result":{"total":19},"time":{…}}`, `Unwrap(res.Result, "total")` = `19`.
- Формы ошибок (9 разных): вложенная — `…METHODNOTFOUNDEXCEPTION` (404),
`…VALIDATION_REQUESTVALIDATIONEXCEPTION` (400, с `validation[{field:"id"}]`),
`…ENTITYNOTFOUNDEXCEPTION`, `…UNKNOWNDTOPROPERTYEXCEPTION`,
`…INVALIDSELECTEXCEPTION`, `…INVALIDFILTEREXCEPTION`, `…INVALIDJSONEXCEPTION`,
`…ACCESSDENIEDEXCEPTION` (401 на неверный токен), а также
`CRM_EMAIL_INVALID_REQUEST` без приставки; плоская — `INTERNAL_SERVER_ERROR`
(500) на том же адресе v3.
- `errors.Is(err, ErrMethodNotFound)` = `true`, `CodeOf` = код v3 как пришёл.
- Списки: у `tasks.task.list` на v3 нет ни `next`, ни `total`; `start`
**молча игнорируется** (та же первая страница), фильтр v1 (`{">ID": …}`)
отклоняется.
- `batch`: работает в формате v3 через `Core.Call` (ответ — массив
`[{"total":19},{"items":[…]}]`); тело батча v1 отклоняется.
- `documentation`: 177 методов, 25 из них доступны по GET. Через `Call` даёт
`Result == nil` без ошибки — конверта у ответа нет.
- v1 на том же портале не изменился: `profile`, `batch`, `ERROR_METHOD_NOT_FOUND`.

Не проверялось: OAuth-авторизация на v3 и `CallMultipart` на v3 (прогон шёл на
вебхуке; v3 заявляет только JSON-тело).

## [0.1.0] — 2026-08-04

Expand Down
147 changes: 147 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,106 @@ _, err := client.App().InstallFinish(ctx, nil)

Используйте его только если сценарий действительно требует серверного вызова. В стандартных сценариях метод вызывается во фронтенде.

## REST API 3.0

Чтобы вызывать методы [REST 3.0](https://apidocs.bitrix24.ru/api-reference/rest-v3.html),
достаточно передать адрес новой версии — с сегментом `/rest/api/` вместо
`/rest/`. Никакой опции для этого нет:

```go
// v1: https://portal.bitrix24.ru/rest/1/токен/
// v3: https://portal.bitrix24.ru/rest/api/1/токен/
client := b24.NewClient("https://portal.bitrix24.ru/rest/api/1/токен/")

res, err := client.Core().Call(ctx, "tasks.task.list", b24.Params{
"select": []string{"id", "title"},
"filter": [][]any{{"id", ">", 500}},
"pagination": b24.Params{"limit": 20, "page": 1},
})
```

Версия выводится из адреса, а не задаётся отдельно, потому что адрес её и так
задаёт: без `/api/` портал выполнит метод старой версии или ответит «метод не
найден». Второй источник истины можно было бы рассогласовать с первым — сказать
«версия 3» и забыть `/api/` в адресе, — и тогда каждый вызов уходил бы в v1, а
разбирался бы по правилам v3.

Для приложения адрес v3 — `https://portal.bitrix24.ru/rest/api/`, токен
по-прежнему уходит в теле запроса.

### Что на v3 работает

Проверено на живом портале (Битрикс24 облачный, август 2026):

- **`Call`, `CallJSON`** — да. Конверт успешного ответа у v3 тот же, что у v1,
поэтому `CallResult`, `Result`, `Kind`, `Unwrap`, `UnwrapFold`, `IsEmpty`, `ID`
работают без изменений.
- **Коды ошибок** — да: `errors.Is`, `CodeOf` и sentinel-ошибки заполняются из
вложенного формата v3 (см. ниже).
- **Повторы и `WithIdempotent`** — да, логика та же. Ошибки инфраструктуры (в том
числе `QUERY_LIMIT_EXCEEDED`) на адресе v3 приходят в **плоском** формате v1,
и SDK разбирает оба.
- **`WithTimeout`, `WithHTTPClient`, `WithRetry`** — да, это транспорт, версии
не касается.

### Что на v3 не работает

- **`Pages` и `Scan`** — `ErrV3WalkUnsupported`. У v3 нет курсора: `start`
игнорируется, `next` и `total` в ответе отсутствуют, страница задаётся
параметром `pagination` (`page`, `limit`, `offset`). Обход не «портится», а
**отказывается стартовать** намеренно: на живом портале `Pages` по
`tasks.task.list` прочитал первую страницу, не увидел `next` и отчитался о
завершённом обходе с `Err() == nil` — 2 строки из 423. Частичная выгрузка,
выглядящая как полная, хуже ошибки. Листайте `Call`-ом:

```go
for page := 1; ; page++ {
res, err := client.Core().Call(ctx, "tasks.task.list", b24.Params{
"select": []string{"id"},
"pagination": b24.Params{"limit": 50, "page": page},
}, b24.WithIdempotent())
if err != nil {
return err
}
items, _ := b24.Unwrap(res.Result, "items")
// пусто — страницы кончились
}
```

- **`Batch`, `CallBatch`, `CallBatchChunked`, `Ref`, `Halt`** —
`ErrV3BatchUnsupported`. Метод `batch` у v3 есть, но это другой протокол:
команды кладутся в **корень** тела как `{"method": …, "query": {…}}`, ответ —
**массив** в порядке отправки (ключи команд отбрасываются), а первая упавшая
команда обрывает весь запрос вместо `result_error`. Пока SDK не говорит на этом
формате, вызывайте его напрямую:

```go
res, err := client.Core().Call(ctx, "batch", b24.Params{
"cnt": b24.Params{"method": "humanresources.employee.count", "query": b24.Params{}},
"tsk": b24.Params{"method": "tasks.task.list", "query": b24.Params{"select": []string{"id"}}},
})
// res.Result = [{"total":19},{"items":[{"id":25}]}] — позиционно
```

- **`CallMultipart`** — не проверялось. v3 заявляет только JSON-тело.
- **OAuth-авторизация на v3** — не проверялась: прогон шёл на вебхуке. Токен
уходит в теле, как и на v1, так что работать должно, но замера нет.

### Список методов v3 — мимо SDK

Портал отдаёт его сам, методом `documentation`, в формате OpenAPI. Но **через SDK
его брать нельзя**: этот метод отвечает самим документом, без конверта
`{"result": …}`, поэтому `Call` вернёт `Result == nil` и **никакой ошибки** —
запрос успешен, а данных нет.

Берите его обычным HTTP-запросом:

```go
resp, err := http.Get(webhookURL + "documentation") // адрес v3, GET, без параметров
```

На проверявшемся портале в документе 177 методов, из них 25 доступны и по GET.

## Ошибки

Ошибки, о которых сообщил портал, возвращаются как `*APIError` — с кодом,
Expand Down Expand Up @@ -608,6 +708,53 @@ if errors.Is(err, b24.Code("CREATE_DYNAMIC_TYPE_RESTRICTED")) { … } // люб
называют те же коды, а `b24.Code(...)` покрывает всё остальное — портал
выпускает новые коды без предупреждения, поэтому набор намеренно открытый.

### Ошибки REST 3.0

У v3 другая форма ответа — код и текст лежат во вложенном объекте
(`{"error":{"code":…,"message":…}}`), а не плоско, — но снаружи это не видно:
`*APIError` заполняется из обеих форм, `errors.Is` и `CodeOf` работают как
прежде. Разбор идёт **по форме тела, а не по версии адреса**, потому что адрес v3
отвечает обеими: ошибки шлюза (в том числе `QUERY_LIMIT_EXCEEDED`, на котором
держатся повторы) приходят в плоском формате v1 и на v3.

Коды у версий разные, и **один** из них SDK сводит к старому — тот, у которого
на обеих версиях одно и то же значение:

```go
// на адресе v3 это true, код на проводе — BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION
errors.Is(err, b24.ErrMethodNotFound)
```

Остальные не сводятся, и это не недоделка. `BITRIX_REST_V3_EXCEPTION_ACCESSDENIEDEXCEPTION`
похож на `ACCESS_DENIED`, но замер показал, что v3 отвечает им и на **неверный
токен вебхука**, где v1 отвечает `INVALID_CREDENTIALS`: один код v3 покрывает два
кода v1. Свести их — значит заставить ветку «права не те, авторизация в порядке»
срабатывать на протухшей авторизации. Поэтому для таких случаев — свои sentinel'ы
`ErrV3Validation`, `ErrV3EntityNotFound`, `ErrV3AccessDenied` и константы
`CodeV3*`; любой не перечисленный код по-прежнему берётся через `b24.Code(...)`.

Приставка `BITRIX_REST_V3_EXCEPTION_` **не универсальна** — не выводите код из
неё. Замер: `crm.deal.timeline.activity.email.list` на плохой `id` отвечает
`CRM_EMAIL_INVALID_REQUEST`, в конверте v3 и без всякой приставки.

`CodeOf` возвращает код **как он пришёл**, без перевода: он идёт в лог, и чужой
код там отправил бы читателя искать строку, которой портал не присылал.
Для ветвления — `errors.Is`, для лога — `CodeOf`.

У ошибок валидации v3 есть то, чего у v1 нет вовсе: список полей, из-за которых
запрос отклонён. Код и текст у всех таких ошибок одинаково общие, так что без
него неизвестно, что именно не так:

```go
var apiErr *b24.APIError
if errors.As(err, &apiErr) {
for _, v := range apiErr.Validation {
log.Printf("поле %s: %s", v.Field, v.Message)
// поле id: Обязательное поле `id` не указано
}
}
```

### Повторы: важно не «временная ли ошибка», а «выполнился ли запрос»

- **`QUERY_LIMIT_EXCEEDED`** (HTTP 503) — это лимитер отказал в вызове **до
Expand Down
Loading
Loading