Skip to content

REST API 3.0: the version from the call URL, nested errors - #21

Merged
ExaltedTrou6 merged 2 commits into
mainfrom
feat/rest-v3
Aug 7, 2026
Merged

REST API 3.0: the version from the call URL, nested errors#21
ExaltedTrou6 merged 2 commits into
mainfrom
feat/rest-v3

Conversation

@ExaltedTrou6

@ExaltedTrou6 ExaltedTrou6 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Not a single REST 3.0 method worked through the SDK. There was one cause: buildURL appended .json unconditionally, and v3 does not accept that suffix.

Measured on a live portal:

…/rest/api/1/TOKEN/documentation        → HTTP 200
…/rest/api/1/TOKEN/documentation.json   → HTTP 404, «Метод `documentation.json` не найден»

What was done

The version is derived from the URL. If the base URL has a /rest/api/ segment, this is v3, and .json is not appended. There is no new public option for it and none is proposed: the caller has to pass a v3 URL in any case, otherwise the portal will run the method of the old version — that is, the URL already states the version, and a second source of truth could be made to disagree with it (say "version 3" and forget /api/, and every call goes to v1 while being decoded by the rules of v3). The version is computed from BaseURL on every call rather than remembered on the client, so that a BaseURL swapped after creation does not leave a stale flag behind.

v1 behaviour has not changed in any respect, and there are regression tests for that: TestV1BaseURLKeepsTheJSONSuffix, TestV1ErrorTaxonomyIsUnchanged, TestPagesStillWorksOnV1.

Parsing v3 errors. On v3 the code and the text sit in a nested object rather than flat. *APIError is filled in from both shapes, and errors.Is, CodeOf and the whole taxonomy work as before.

The shape is chosen by the type of the error field, not by the version of the URL — because a v3 URL answers with both. Measured: …/rest/api/…/tasks.task.get with {"id":"abc"} → HTTP 500 and a flat {"error":"INTERNAL_SERVER_ERROR","error_description":"Internal server error"}; this is reported by the REST gateway that sits in front of the v3 controller. Parsing by version would have lost the code of every such error, including QUERY_LIMIT_EXCEEDED, which is what retries rest on.

v3 codes are not translated, and exactly one is folded. errors.Is(err, ErrMethodNotFound) fires on BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION as well — the same situation, the same meaning. The rest are not folded, and that is not an omission:

  • …_ACCESSDENIEDEXCEPTION looks like ACCESS_DENIED, but v3 answers with it on a wrong webhook token too, where v1 answers INVALID_CREDENTIALS. One v3 code covers two v1 codes; folding them would make the branch "the rights are wrong, the credentials are fine" fire on dead credentials.
  • The BITRIX_REST_V3_EXCEPTION_ prefix is not universal: crm.deal.timeline.activity.email.list on a bad id answers CRM_EMAIL_INVALID_REQUEST in the same envelope. Any "strip the prefix and map it" scheme is built on a false premise.

So CodeOf returns the code as it arrived (it goes into a log), and for the cases that cannot be folded there are ErrV3Validation, ErrV3EntityNotFound, ErrV3AccessDenied and the CodeV3* constants.

APIError.Validation — the fields v3 rejected the request over. The code and the text of all such errors are equally generic, so the list of fields is the only part that says what exactly is wrong.

Pages/Scan and CallBatch/CallBatchChunked refuse to work on v3ErrV3WalkUnsupported, ErrV3BatchUnsupported, before the request is sent.

This is not caution, it is preventing a silent loss of data. v3 has no cursor: start is silently ignored, next and total are absent. On a live portal Pages over tasks.task.list read the first page, saw no next and reported a finished walk with Err() == nil — 2 rows out of 423. A partial export that looks like a complete one is exactly what a walk must never have.

v3 does have a batch method, but it is a different protocol: the commands go in the root of the body as {"method": …, "query": {…}}, the reply is an array in submission order (the keys are discarded), and the first failing command aborts the whole request instead of producing result_error. Batch, Ref, Halt, BatchResult have nothing to map onto. The refusal replaces the portal's own answer to a v1 batch body — …INVALIDSELECTEXCEPTION, «Не удается распознать выражение select», for a request that has no select at all. Both sentinels name what to use instead.

Verified on a live portal

Everything below is a cloud portal's answer, taken with calls made by the SDK itself.

What Result
.json on v3 200 without the suffix, 404 with it
The success envelope {"result":{"total":19},"time":{…}} — the same as v1's; Unwrap works
Error shapes 9 different ones: 8 nested (CRM_EMAIL_INVALID_REQUEST with no prefix among them) + a flat INTERNAL_SERVER_ERROR on a v3 URL
errors.Is(ErrMethodNotFound) true on the v3 code; CodeOf — the v3 code as it arrived
Validation [{Field:id Message:Обязательное поле \id` не указано}]`
Lists neither next nor total; start is ignored; a v1 filter is rejected
batch on v3 works through Core.Call, the reply is [{"total":19},{"items":[…]}]
documentation 177 methods, 25 of them over GET; through Call it gives Result == nil with no error — there is no envelope, which is why the docs say to fetch it with a plain http.Get
v1 on the same portal profile, batch, ERROR_METHOD_NOT_FOUND — unchanged

Not verified: OAuth authorization on v3 and CallMultipart on v3 (the run went over a webhook; v3 declares a JSON body only). The documentation calls this untested rather than working.

Local checks

go build ./..., go vet ./..., gofmt -l . (empty), go test -race ./... — all green locally. Checked locally because the repository's CI runs were hanging in queued and did not start; on this PR CI did run after all and is green (stable and go.mod (minimum)).

Merge order

The branch is off main. It merges cleanly with #20. After merging #19#20#17#18 there remain two trivial "both added next to each other" conflicts, both resolved by keeping both sides:

🤖 Generated with Claude Code

ExaltedTrou6 and others added 2 commits August 7, 2026 09:35
The SDK could not call a single REST 3.0 method: buildURL appended .json
unconditionally, and v3 answers 404 for the suffix. It now appends it only for
v1, choosing the version by the /rest/api/ segment of the base URL.

The URL is the only source of truth on purpose. A caller has to pass a v3 URL
anyway — without /api/ the portal runs the v1 method of that name — so an
option would be a second place to state the version, and could disagree with
the first.

Errors: v3 nests code and message in an object where v1 has a flat string. The
parser now picks the shape by the JSON TYPE of "error", not by the version of
the URL, because a v3 URL answers in BOTH: the REST gateway in front of the v3
controller reports in the flat v1 form, and QUERY_LIMIT_EXCEEDED — which the
retry loop depends on — comes from that gateway. APIError also carries the
per-field Validation array, the only actionable part of a v3 validation error.

One v3 code is folded onto its v1 sentinel so errors.Is keeps working:
METHODNOTFOUNDEXCEPTION -> ERROR_METHOD_NOT_FOUND. The rest are not, because
measurement says they are not the same sets — v3 answers a wrong webhook token
with ACCESSDENIEDEXCEPTION where v1 answers INVALID_CREDENTIALS. CodeOf keeps
returning the code that arrived.

Pages, Scan and CallBatch refuse a v3 client rather than half-work. v3 ignores
start and sends no next, so Pages read the first page of tasks.task.list on a
live portal and reported a finished walk with Err() == nil: 2 rows out of 423.
The v3 batch is a different protocol — commands at the top level, a positional
array back, no per-command errors — so Batch/Ref/Halt have nothing to map onto.
Both sentinels name what to call instead.

v1 behavior is unchanged, and has its own regression tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	batch.go
@ExaltedTrou6
ExaltedTrou6 merged commit 307c605 into main Aug 7, 2026
2 checks passed
@ExaltedTrou6 ExaltedTrou6 changed the title REST API 3.0: версия из адреса вызова, вложенные ошибки REST API 3.0: the version from the call URL, nested errors Aug 7, 2026
@ExaltedTrou6
ExaltedTrou6 deleted the feat/rest-v3 branch August 7, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant