26 мая 2026 · 1С, ODATA, HTTP · 9 мин

Подводные камни и рецепты 1С ODATA

Как пользоваться разделом

Каждый пункт сформулирован как симптом → причина → что делать. Внизу — рецепты для типовых сценариев. Если ищешь конкретный код ошибки 1С — см. endpoints.md §3.

Содержание

  1. Quirks: query-параметры
  2. Quirks: $filter и функции
  3. Quirks: форматы и метаданные
  4. Quirks: имена, суффиксы, кодировка
  5. Quirks: мутации (POST/PATCH/PUT/DELETE)
  6. Quirks: права доступа и allowedOnly
  7. Рецепты типовых сценариев

1. Quirks: query-параметры

1.1. $inlinecount=allpages гасит $top и $skip

Симптом. Запрос ?$top=20&$skip=40&$inlinecount=allpages — в ответе все записи и поле odata.count. Пагинация не сработала.

Причина. 1С (как и спецификация OData v3) трактует $inlinecount как «вернуть всё + общее число»; $top/$skip при этом игнорируются.

Что делать. Считать и листать разными запросами:

  • GET /Catalog_X/$count?$filter=... → число (text/plain).
  • GET /Catalog_X?$filter=...&$top=20&$skip=40&$orderby=... → страница.

1.2. $expand не работает на одиночной сущности

Симптом. GET /Catalog_X(guid'...')?$expand=Контрагент — параметр молча игнорируется, навигационное свойство не разворачивается.

Причина. reference §17.4.6.4: $expand запрещён на single-entity, на табличных частях и на одиночных записях регистров.

Что делать. Либо через коллекцию ?$filter=Ref_Key eq guid'...'&$expand=...&$top=1, либо отдельным GET после чтения _Key.

1.3. $expand на составном реквизите требует явного типа

Симптом. $expand=Договор/* — ошибка 14 или пустой результат.

Причина. Составной реквизит может ссылаться на разные типы; OData не знает, какой из них раскрывать.

Что делать. Указать тип явно: $expand=Договор/Document_ДоговорНайма/*. Развёрнутое значение придёт в свойство с суффиксом _Expanded.

1.4. $expand на виртуальной таблице — отклонение от OData v3

Симптом. Balance?$expand=Контрагент — поле возвращается, но не туда, куда ждёт стандартный OData-клиент.

Причина. Для измерений 1С кладёт развёрнутый объект рядом с _Key (Контрагент_Key + Контрагент); для составных типов — в свойство с суффиксом _Expanded. Это не стандарт.

Что делать. Парсер клиента должен знать про _Expanded и про возможность «измерение + развёрнутый объект» рядом.

1.5. allowedOnly работает только на коллекциях

Симптом. Добавил ?allowedOnly=true на single-entity или на POST/PATCH — игнорируется, ловишь 401 или код 20.

Причина. Параметр применим только к GET коллекций (entity-set).

Что делать. На одиночных эндпоинтах либо иметь права на конкретный объект, либо предварительно отфильтровать через коллекцию.

1.6. ____Presentation не попадает в * и **

Симптом. ?$select=* или ** — поля ____Presentation отсутствуют.

Причина. reference §17.4.3: представления не входят в */** по умолчанию.

Что делать. Перечислить явно ($select=*, Контрагент____Presentation) или взять представления всех реквизитов ($select=*, *____Presentation).


2. Quirks: $filter и функции

2.1. Сравнение составного реквизита с guid возвращает пусто

Симптом. $filter=Документ eq guid'...' — результат всегда пуст, даже когда совпадение есть.

Причина. Составной реквизит нельзя сравнивать с guid напрямую — нужен cast().

Что делать:

$filter=Документ eq cast(guid'...', 'Document_ПриходнаяНакладная')

2.2. Не работают length, indexof, replace, tolower, toupper, trim

Симптом. Ошибка 21 на этих стандартных OData-функциях.

Причина. 1С их не реализовала.

Что делать. Заменять на like(...), startswith(...), endswith(...), substring(...), substringof(...). Сравнение без учёта регистра — на клиенте или через like с шаблонами.

2.3. Не работают years, days, hours, seconds, floor, ceiling

Симптом. Ошибка 21.

Причина. 1С реализовала единственные year/month/day/hour/minute/second/quarter/dayofweek/dayofyear, плюс dateadd/datedifference. Множественных аналогов и floor/ceiling нет.

Что делать. Использовать только реализованные имена. Разности дат — через datedifference(d1, d2, 'day').

2.4. Лямбда без any / all не работает

Симптом. Хочу отфильтровать по реквизиту строки табличной части — $filter=Товары/Цена gt 10000 → ошибка 21.

Причина. Коллекции (табличные части) фильтруются только через any/all с лямбда-переменной.

Что делать:

$filter=Товары/any(d: d/Цена gt 10000)

2.5. Condition= в виртуальной таблице — $filter в одинарных кавычках

Симптом. Ошибка 14 при попытке передать Condition=Валюта_Key eq guid'...' без обёртки.

Причина. Значение Conditionстрока, поэтому требует одинарных кавычек снаружи, а guid-литералы внутри экранируются удвоением кавычек.

Что делать:

.../SliceLast(Period=datetime'2024-01-01T00:00:00',Condition='Валюта_Key eq guid''aaa-bbb-...''')

2.6. Цепочка cast() для отбора через составной реквизит

Симптом. Хочу отфильтровать МаршрутныйЛист по складу из накладной — реквизит ОснованиеОтгрузки составной (Накладная | ВнутреннееПеремещение), а МестоОтгрузки накладной — тоже составной (Склад | АдресаОтгрузки).

Что делать:

cast(cast(ОснованиеОтгрузки, 'Document_Накладная')/МестоОтгрузки, 'Catalog_Склад')/Название eq 'Основной'

3. Quirks: форматы и метаданные

3.1. $format=json игнорируется на $metadata

Причина. CSDL — только XML. Управлять детализацией json-ответов сущностей (не метаданных) можно через $format=application/json;odata=<level>, но это про данные.

Что делать. Парсить $metadata только как XML.

3.2. $format=json игнорируется на SelectChanges

Причина. План обмена возвращает atom-feed + <at:deleted-entry> (RFC 6721). JSON-варианта не предусмотрено.

Что делать. Держать отдельный atom-парсер для SelectChanges.

3.3. odata=nometadata ломает последующий PATCH

Симптом. Взял ответ как есть (с nometadata) и попытался отправить его PATCH'ем обратно — ошибка 4 или 16.

Причина. При nometadata пропадает @odata.type у коллекций (табличные части, RecordSet), и при последующем PATCH сервер не знает, во что разворачивать массив.

Что делать. Для read-modify-PATCH использовать минимум odata=minimalmetadata (значение по умолчанию) или fullmetadata.


4. Quirks: имена, суффиксы, кодировка

4.1. Кириллица в URL должна быть percent-encoded

Симптом. Ручная сборка URL с Catalog_Товары без кодирования — на некоторых прокси/балансировщиках 400.

Причина. В URI допустим только ASCII; всё остальное — percent-encoded байты.

Что делать. httpx / requests кодируют автоматически. При ручной сборке — urllib.parse.quote.

4.2. Одинарная кавычка в строке — удвоением

Причина. В OData v3 нет backslash-экранирования; одинарная кавычка внутри литерала строки записывается как ''.

Что делать. 'O''Brien', 'это ''цитата'''.

4.3. Стандартные реквизиты в dot-traversal — только английские

Симптом. /Document_X(guid'...')/Контрагент/Наименование — ошибка 10.

Причина. В dot-path для стандартных реквизитов 1С использует английские имена.

Что делать. Description, Code, Ref_Key, DataVersion, Posted, Date, Number, DeletionMark, IsFolder, Parent.

4.4. В PUT ссылки через @odata.bind, в POST — через _Key

Причина. Разные контракты: POST создаёт по плоским полям, PUT перезаписывает целиком и требует навигационных ссылок.

Что делать.

  • POST: "Контрагент_Key": "<guid>".
  • PUT: "Контрагент@odata.bind": "Catalog_Контрагенты(guid'<guid>')". Можно полный URI или короткий путь после standard.odata.

4.5. Составной реквизит при записи — пара значение + _Type

Симптом. POST/PATCH с составным реквизитом — поле сохраняется как «Неопределено».

Причина. Для составного типа сервер ждёт значение и диспетчеризационное свойство <Реквизит>_Type со строкой типа ("StandardODATA.Catalog_Контрагенты").

Что делать. Всегда писать пару. Для «Неопределено»: _Type = "StandardODATA.Undefined", само значение игнорируется.

4.6. Ref_Key против Контрагент_Key

Ref_Key — ссылка на саму сущность (первичный ключ). <Реквизит>_Key — суффикс свойства, содержащего значение ссылки на связанную сущность. В $filter для отбора по связанному реквизиту использовать Контрагент_Key eq guid'...' (а не Контрагент eq guid'...').


5. Quirks: мутации (POST/PATCH/PUT/DELETE)

5.1. Прямой POST на табличную часть запрещён

Симптом. POST /Document_X_Товары с телом строки — ошибка 13.

Что делать. PATCH родителя с полной перезаписью массива Товары.

5.2. Табличная часть пишется целиком

Причина. OData v3 не поддерживает merge коллекций по ключу строки; 1С не реализовала ничего сверху.

Что делать. Прочитать родителя → локально модифицировать массив → PATCH с полным массивом + аннотацией <TP>@odata.type. См. endpoints.md §8.3.

5.3. Регистр бухгалтерии — только read-modify-PATCH

Причина. Записи регистра бухгалтерии — набор по регистратору; OData не даёт писать запись по отдельности.

Что делать. GET → модификация RecordSet → PATCH с обязательной "шапкой" (odata.type, Recorder_Key, RecordSet@odata.type). См. endpoints.md §8.4.

5.4. Posted через PATCH не работает

Причина. Проведение — действие, а не флаг.

Что делать. POST .../Post?PostingModeOperational=false. Для отмены — POST .../Unpost.

5.5. DELETE удаляет физически

Симптом. Ожидал помеченный объект — на самом деле его нет в базе.

Что делать. Для пометки — PATCH {"DeletionMark": true}. DELETE — только когда нужно физическое удаление.

5.6. If-Match: <DataVersion> хранить байт-в-байт

Причина. DataVersion — opaque-строка; пересоздавать, нормализовать, обрезать пробелы — нельзя.

Что делать. Хранить как получено. При 412 — перечитать сущность и повторить с новым значением.

5.7. 1C_OData-DataLoadMode: true — только для миграции

Причина. Заголовок отключает проверки прикладного решения: валидации, автозаполнения, движения регистров.

Что делать. Не использовать в обычных операциях. Только при синхронизации/миграции, когда данные заведомо целостны. Не все объекты поддерживают режим — попытка вернёт код 17.

5.8. POST на сущность вместо коллекции → код 19

Причина. POST принимает только entity-set URL. POST на single-entity бессмыслен — для обновления есть PATCH/PUT.

Что делать. Создавать — POST B/Catalog_X (без ключа). Обновлять — PATCH B/Catalog_X(guid'...').


6. Quirks: права доступа и allowedOnly

6.1. Без allowedOnly=true запрос может упасть в 401

Причина. При выборке, попадающей под ограничение доступа к данным, 1С возвращает 401 (внутренний код 20). Это не аутентификация — это RLS.

Что делать. Для GET коллекций — добавлять ?allowedOnly=true. Для одиночных — иметь права на конкретный объект или предварительно отфильтровать.

6.2. allowedOnly=false + явный фильтр под разрешённые тоже работает

Симптом. allowedOnly=false, но запрос проходит — почему?

Причина. Если явный $filter оставляет в выборке только разрешённые данные, 401 не возникает.

Что делать. Можно так, но для устойчивости проще всегда ставить allowedOnly=true на коллекциях.


7. Рецепты типовых сценариев

7.1. Постраничный список + общее число

GET B/Catalog_Товары/$count?$filter=Цена gt 500
→ 19

GET B/Catalog_Товары?$filter=Цена gt 500&$top=20&$skip=0&$orderby=Description&allowedOnly=true
→ страница 1

Не использовать $inlinecount=allpages совместно с $top/$skip (см. 1.1).

7.2. Чтение составного реквизита

В ответе сущности с составным реквизитом Документ:

  • Документ — значение (guid или пустое, в зависимости от типа).
  • Документ_Type — строка вида "StandardODATA.Document_ПриходнаяНакладная" или "StandardODATA.Undefined".

Чтобы получить связанный объект — прочитать _Type, затем второй GET по соответствующему эндпоинту. Inline-$expand требует явного типа в пути.

7.3. Запись табличной части

PATCH B/Catalog_Магазины(guid'...')?$format=json
Content-Type: application/json

{
  "ТорговыеЗалы@odata.type": "Collection(StandardODATA.Catalog_Магазины_ТорговыеЗалы_RowType)",
  "ТорговыеЗалы": [
    {"LineNumber": 1, "Название": "Синий", "Площадь": 56},
    {"LineNumber": 2, "Название": "Красный", "Площадь": 64}
  ]
}

Полный массив, даже если меняется одна строка.

7.4. Запись набора регистра бухгалтерии

# 1. Read
GET B/AccountingRegister_X(guid'<recorder>')?$format=json

# 2. Modify RecordSet locally

# 3. Write
PATCH B/AccountingRegister_X(guid'<recorder>')?$format=json
{
  "odata.type": "StandardODATA.AccountingRegister_X_RowType",
  "Recorder_Key": "<recorder>",
  "RecordSet@odata.type": "Collection(StandardODATA.AccountingRegister_X_RowType)",
  "RecordSet": [ /* строки */ ]
}

7.5. Optimistic locking

# Read
GET B/Catalog_X(guid'...')?$format=json
→ {... "DataVersion": "AAAAA...==", ...}

# Update with check
PATCH B/Catalog_X(guid'...')?$format=json
If-Match: AAAAA...==
{...}

# 412 → перечитать, повторить

7.6. Фильтр по составной ссылке

$filter=ДокументОснование eq cast(guid'aaa-bbb', 'Document_РасходТовара')

Проверка только типа (без значения):

$filter=isof(ДокументОснование, 'Document_РасходТовара')

7.7. Проведение документа

POST B/Document_РасходТовара(guid'...')/Post?PostingModeOperational=false

Тела нет. В ответе — обновлённый документ с Posted: true. Для отмены — POST .../Unpost без параметров.

7.8. Получение изменений из плана обмена

POST B/SelectChanges?DataExchangePoint='B/ExchangePlan_X(guid'<node>')'&MessageNo=42

Парсить как atom-feed: <entry> — новые/изменённые элементы, <at:deleted-entry> — удалённые (RFC 6721). После обработки — POST B/NotifyChangesReceived?... с тем же MessageNo.

7.9. Срез последних по подчинённому регистру сведений

GET B/InformationRegister_КурсыВалют_RecordType/SliceLast?Period=datetime'2024-01-01T00:00:00',Condition='Валюта_Key eq guid''aaa-bbb-...'''&$format=json

Обратить внимание: для подчинённого регистра функция вызывается на _RecordType, не на самом регистре.

7.10. Пометка на удаление vs физическое удаление

# Пометка
PATCH B/Catalog_X(guid'...')?$format=json
{"DeletionMark": true}

# Физическое удаление
DELETE B/Catalog_X(guid'...')
If-Match: <DataVersion>

DELETE не помечает — удаляет сразу. Снять пометку — PATCH {"DeletionMark": false}.


См. также