Net-Base Журнал

09.06.2026

REST API с RemObjects SDK: корректно версионировать и отлаживать JSON-эндпоинты (Delphi фрагменты исходного кода)

Как с помощью RemObjects SDK в Delphi создать REST API, которое не развалится в эксплуатации: стабильные JSON-контракты, версионирование без разрастания URL, Correlation-ID через все слои, централизованное сопоставление ошибок, snapshot-логирование для трудных случаев отладки, а также практические рекомендации.

09.06.2026

От темы в журнале к проектной практике

Соответствующие страницы услуг и технологий к статье

Почему «REST API с RemObjects SDK» в практической эксплуатации часто решает на стыках

Одна REST API с RemObjects SDK редко выигрывает или проигрывает на примере «Hello World»-сервиса; решающие моменты возникают там, где сталкиваются эксплуатация, наследие и интеграция: версионирование без простоя, единообразное поведение при ошибках во всех эндпоинтах, воспроизводимая отладка при цепочках прокси и способность однозначно коррелировать запросы в случае проблем.

RemObjects SDK предоставляет для этого много инфраструктуры: сервисы, форматы сообщений, сериализацию, хостинг (например, как Windows- и Linux-Services или за IIS/Reverse Proxy) и определённые точки для централизованной обработки ошибок. Чего в зрелых ландшафтах бизнес‑приложений часто не хватает, так это последовательно соблюдаемого контракта: какие поля JSON являются стабильными? Как мы сигнализируем об ошибках? Как распознать запрос, если он прошёл через балансировщик нагрузки, TLS-терминацию и несколько слоёв бэкенда?

Следующий подход (включая Delphi-Snipsel) показывает надёжную линию для RemObjects SDK: версионировать JSON-контракты, принуждать Correlation-ID (Request-ID для отслеживания), переводить исключения в HTTP-статусы и JSON-объекты ошибок, при этом не противопоставлять отладку и эксплуатацию. Дополнительно рассматриваем граничные случаи, которые регулярно встречаются в реальных средах: многопоточность на сервере, доступы к базе данных с BDE-заменой с нативным подключением, заголовки прокси, таймауты и «грязные» payloadы клиентов.

Архитектурное решение: версионирование через Media Type вместо URL

Многие API версионируют через пути, например /v1/. Это прагматично, но в долгоживущих интеграциях (например, интеграции ERP/DMS/CRM) это часто приводит к дублированию URL, дублированию маршрутов, дублированию тестов и к вопросу «какую версию мы вообще используем?» в эксплуатационной документации.

Альтернатива — версионирование через Media Type (Content Negotiation). Клиент, например, отправляет Accept: application/vnd.company.order+json;v=2. Сервер детерминистически извлекает версию и адаптирует поведение контрактов/DTO. Это работает в цепочках прокси и кэшей при условии, что заголовки корректно проксируются. Для администраторов это также удобно проверять: запрос можно воспроизвести через Curl/Postman, не меняя URL.

RemObjects SDK не «REST-пуристичен», а прагматичный сервис‑фреймворк. Именно поэтому вариант с медиатипом оправдан: вы сохраняете стабильные эндпоинты и при этом можете развивать контракты. Важно всегда извлекать версию, принимать решение централизованно и помещать результат в контекст сервиса.

Когда проваливается вариант с Accept‑заголовком?

На практике есть три типичных точки отказа, которые следует заранее учесть:

  • Proxy-Policies: некоторые Reverse Proxies/WAF‑правила нормализуют или фильтруют Accept‑заголовки. Тогда ваша API тихо падает на значение по умолчанию. Решение: явно проверить правила прокси, при необходимости перейти на X-Api-Version.
  • Client-Libraries: некоторые HTTP‑клиенты устанавливают свои Accept‑заголовки и перезаписывают значения. Решение: поддерживать версию контракта также в виде необязательного параметра запроса (только как fallback) или парсить Accept‑заголовок на сервере терпимо.
  • Caching: Если используется кэширование ответов, кэш должен варьировать по заголовку Accept (Vary: Accept), иначе он отдает версию 1 клиентам версии 2. Решение: явно установить Vary или отключить кэширование на уровне API.
  • Фрагмент исходника: Request-Context, Correlation-ID, версия и Error-Mapping

    Код намеренно оформлен так, чтобы его можно было интегрировать в существующие RemObjects-Serverprojekte: небольшой слой Context, парсер версии API (из Accept), механизм Correlation-ID и центральное Exception-Mapping. Термины:

    • Correlation-ID: Уникальный идентификатор для каждого запроса, который возвращается в ответе и используется в логах.
    • Exception-Mapping: Преобразование внутренних Delphi-исключений в стабильные, клиент‑обрабатываемые объекты ошибок (включая HTTP‑статус).
    • Contract-Version: Версия JSON-контракта, определяющая поведение и поля.
    Delphi
    unit Api.Infrastructure;
    
    interface
    
    uses
      System.SysUtils, System.Classes, System.StrUtils, System.Generics.Collections,
      System.JSON;
    
    type
      EApiError = class(Exception)
      private
        FHttpStatus: Integer;
        FCode: string;
        FCorrelationId: string;
      public
        constructor Create(const AHttpStatus: Integer; const ACode, AMessage, ACorrelationId: string);
        property HttpStatus: Integer read FHttpStatus;
        property Code: string read FCode;
        property CorrelationId: string read FCorrelationId;
      end;
    
      TApiContext = record
        CorrelationId: string;
        ContractVersion: Integer;
        RemoteIp: string;
        UserAgent: string;
        class function New: TApiContext; static;
      end;
    
      TApiVersion = record
        class function FromAcceptHeader(const AAccept: string; const ADefault: Integer = 1): Integer; static;
      end;
    
      TApiErrorMapper = class
      public
        class function ToErrorJson(const E: Exception; const ACorrId: string): TJSONObject; static;
        class function ToHttpStatus(const E: Exception): Integer; static;
        class function SafeMessage(const E: Exception): string; static;
      end;
    
    implementation
    
    { EApiError }
    
    constructor EApiError.Create(const AHttpStatus: Integer; const ACode, AMessage, ACorrelationId: string);
    begin
      inherited Create(AMessage);
      FHttpStatus := AHttpStatus;
      FCode := ACode;
      FCorrelationId := ACorrelationId;
    end;
    
    { TApiContext }
    
    class function TApiContext.New: TApiContext;
    begin
      Result.CorrelationId := '';
      Result.ContractVersion := 1;
      Result.RemoteIp := '';
      Result.UserAgent := '';
    end;
    
    { TApiVersion }
    
    class function TApiVersion.FromAcceptHeader(const AAccept: string; const ADefault: Integer): Integer;
    // Ожидает, например: application/vnd.company.order+json;v=2
    var
      Parts: TArray<string>;
      P: string;
      V: string;
      I: Integer;
    begin
      Result := ADefault;
      if AAccept.Trim.IsEmpty then
        Exit;
    
      Parts := AAccept.Split([';', ',']);
      for P in Parts do
      begin
        V := Trim(P);
        if StartsText('v=', V) then
        begin
          if TryStrToInt(Copy(V, 3, MaxInt), I) and (I > 0) and (I < 100) then
            Exit(I);
        end;
      end;
    end;
    
    { TApiErrorMapper }
    
    class function TApiErrorMapper.SafeMessage(const E: Exception): string;
    // В продакшене — никаких внутренних подробностей, никаких SQL или путей.
    // Для отладки/стейджинга это можно расширить через конфигурацию.
    begin
      if E is EApiError then
        Exit(E.Message);
    
      if E is EArgumentException then
        Exit('Неверные параметры.');
    
      Exit('Внутренняя ошибка.');
    end;
    
    class function TApiErrorMapper.ToHttpStatus(const E: Exception): Integer;
    begin
      if E is EApiError then
        Exit(EApiError(E).HttpStatus);
    
      if E is EArgumentException then
        Exit(400);
    
      Exit(500);
    end;
    
    class function TApiErrorMapper.ToErrorJson(const E: Exception; const ACorrId: string): TJSONObject;
    var
      Code: string;
      Status: Integer;
      Msg: string;
    begin
      Status := ToHttpStatus(E);
      Msg := SafeMessage(E);
    
      if E is EApiError then
        Code := EApiError(E).Code
      else if E is EArgumentException then
        Code := 'bad_request'
      else
        Code := 'internal_error';
    
      Result := TJSONObject.Create;
      Result.AddPair('error', TJSONObject.Create
        .AddPair('code', Code)
        .AddPair('message', Msg)
        .AddPair('httpStatus', TJSONNumber.Create(Status))
        .AddPair('correlationId', ACorrId));
    end;
    
    end.

    Цель: стабильный контекст запроса вместо «где‑то в ThreadLocal»

    Фрагмент сознательно разделяет: TApiContext — это минимальное состояние, которое вы должны передавать. В RemObjects SDK многое работает через контекст сервера/канала. В разнородных проектах (например, дополнительные worker‑потоки, очередь БД, фоновые задания) явная передача контекста часто более надёжна, чем неявные ThreadLocal‑переменные, потому что она делает параллелизм и переключения контекста более очевидными.

    Предпосылки: Вариант с Accept‑заголовком предполагает, что ваш обратный прокси (nginx, IIS ARR, Traefik) пересылает заголовок без изменений. В некоторых окружениях «необычные» Accept‑заголовки фильтруются или объединяются.

    Подводные камни: Версионирование через Accept работает ровно настолько хорошо, насколько хороши ваши тесты. Если клиенты используют библиотеки, перезаписывающие Accept, API может внезапно вернуться к значению по умолчанию. Для legacy‑клиентов полезен дефолтный фоллбек, но он должен быть виден в мониторинге (например, предупреждение в логах «Version defaulted»).

    Варианты: Если вы предпочитаете версионирование через X-Api-Version: парсер тот же, меняется только источник — другой заголовок. С точки зрения шлюзов это иногда проще контролировать.

    Интеграция в RemObjects SDK: Correlation‑ID и маппинг исключений на входе сервиса

    Реальный эффект достигается, если вы применяете механизм последовательно на краях сервера: один раз при входе запроса — читать из заголовков, один раз при выходе из исключения — переводить в стабильный ответ. В зависимости от хостинга (например, RO‑HTTP‑Server, IIS‑hosting, самостоятельно управляемые Windows-/Windows- и Linux-Services) конкретные точки привязки отличаются; принцип остаётся тем же: строить контекст, вызывать бизнес‑логику, централизованно сопоставлять исключения.

    Во многих проектах на RemObjects работу часто ведут прямо в каждом методе сервиса. Сначала это хорошо масштабируется, но в эксплуатации даёт сбои: каждая метода строит логирование и обработку ошибок по‑разному. Чёткий разъём — базовый сервис или диспетчер, который обеспечивает стандартизацию.

    Практический порядок действий (сознательно коротко и близко к реализации)

    1. Читать Correlation‑ID из заголовка запроса X-Correlation-ID; если отсутствует — генерировать на сервере (например, GUID).
    2. Читать версию контракта из Accept (или из X-Api-Version).
    3. Логировать начало запроса: метод, путь, Correlation‑ID, удалённый IP, запустить измерение длительности.
    4. Выполнить бизнес‑логику; доступы к БД по возможности оборачивать транзакционно.
    5. Перехватить исключения: определить HTTP‑статус, сформировать JSON‑объект ошибки, установить в заголовок ответа X-Correlation-ID.
    6. Логировать окончание запроса: статус, длительность, при необходимости код ошибки.

    Потоковая модель сервера: почему Correlation‑ID без дисциплины контекста становится бесполезной

    Распространённый крайний случай Delphi: метод сервиса запускает асинхронную работу (например, генерация отчёта, импорт, пуш в DMS). Тогда исходный поток запроса перестаёт быть тем, который позже пишет строки лога. Если Correlation‑ID известна только «в начале», трассировка распадается.

    Прагматическое правило: всё, что не остаётся строго в потоке запроса, должно получать контекст явно в параметрах. Даже если это делает списки параметров длиннее, это окупается. В качестве альтернативы можно использовать явно определённый объект контекста, который целенаправленно передаётся воркерам (вместо глобальных переменных или скрытых синглтонов).

    Типичные точки срыва в серверах RemObjects/Delphi:

    • DB-подключения на поток: BDE-Ablosung mit nativer Anbindung-подключения не всегда автоматически безопасно разделяются между потоками. Пул подключений или отдельное подключение на поток часто разумнее, чем «одно глобальное подключение».
    • Границы транзакций: Если в рамках одного запроса выполняется несколько взаимосвязанных шагов, транзакция должна оставаться в той же логической единице. Асинхронная работа не должна «по случайности» продолжаться в той же транзакции.
    • Отмена: Если клиент прерывает запрос (таймаут прокси, закрытый браузер), сервер часто продолжает обработку. Осознанно решите, имеет ли тогда смысл фоновая работа.

    Доступ к данным и коды ошибок: 409 — это не «просто 500»

    В интеграционных проектах корректное отображение ошибок — это не косметика. От этого зависит, сможет ли партнер (ERP-коннектор, ETL-джоб, портал для клиентов) правильно отреагировать. Несколько практических ориентиров, проверенных в Delphi/RemObjects-средах:

    • 400 Bad Request: Валидация, отсутствующие/некорректные параметры, нераспарсиваемый JSON. Важно: ответ должен оставаться стабильным, даже если тело повреждено.
    • 401/403: Разделяйте аутентификацию и авторизацию. 401 означает «нет/недействительная идентичность», 403 — «идентичность OK, но доступ запрещён».
    • 404: Ресурс не существует. Осторожно с вопросами безопасности: не всегда стоит раскрывать, существует ли что-то.
    • 409 Conflict: Доменный/функциональный конфликт (например, конфликт версий, «статус не позволяет выполнить действие», нарушение уникального ключа, если оно имеет смысл с точки зрения предметной области).
    • 422 Unprocessable Content: Когда синтаксически всё в порядке, но проваливается предметно-ориентированная валидация (не каждая команда использует 422, но он часто яснее, чем 400).
    • 500: Всё, что не удалось чётко классифицировать. Сюда относится и «БД недоступна», и «таймаут», и «необработанное исключение».

    Delphi-специфический приём: многие ошибки БД всплывают как общие исключения. Стоит в слое доступа к данным целенаправленно распознавать известные ситуации и переводить их в EApiError. Важно: не включайте фрагменты SQL или внутренние имена таблиц/столбцов в сообщение клиенту. Эти детали должны идти в лог, а не в ответ.

    Приём для отладки: воспроизводимые ошибки через «Contract Snapshot»

    Нетривиально, но в эксплуатации крайне полезно: при ошибках (или выборочно по определённым Correlation-IDs) сохраняйте «снэпшот» из заголовков запроса + тела запроса в файл отладочного спула. Это не постоянный лог (вопросы конфиденциальности/объёма), а контролируемый инструмент для воспроизведения трудноуловимых случаев из близкой к продакшену среды.

    Важно: снэпшот никогда не должен без фильтрации сохранять Auth-Header, токены или персональные данные. На практике это означает: редактирование (маскирование) и включение только через feature-flag или белый список (например, только для отдельных Correlation-IDs, в короткие временные окна).

    Корректная реализация на практике: маскирование вместо удаления

    В реальных интеграциях именно «критичные» поля часто нужны для отладки (например идентификаторы). Вместо тотального удаления лучше маскировать: частично заменять токены, в e‑mail оставлять только домен, в IBAN — только последние цифры. Так инцидент остаётся воспроизводимым, без лишнего распространения данных в файловой системе. Дополнительно снэпшот должен быть явно помечен как отладочный артефакт и иметь заданный срок хранения.

    Безопасность и эксплуатация: передача заголовков, цепочки прокси и таймауты

    Одна REST API редко завершается прямо у клиента. Типично встречаются цепочки из обратного прокси, TLS-терминации, WAF или API-шлюза. Из этого следуют практические выводы:

    • Remote IP: Не полагайтесь слепо на X-Forwarded-For. Принимайте его только от доверенных прокси, в остальных случаях используйте прямой IP сокета. В эксплуатационных руководствах должно быть указано, какие хопы считаются доверенными.
    • Timeouts: Если у прокси таймаут 30 секунд, а вашему бэкенду требуется 2 минуты, это порождает призрачные запросы. Установите таймауты согласованно по всей цепочке и примите решение: синхронный запрос или паттерн job (202 Accepted + эндпойнт статуса).
    • Correlation-ID: Присылайте Correlation-ID в заголовках ответа, чтобы администраторы могли сопоставить её с логами и на стороне клиента. Если шлюз использует собственные Request-IDs, логируйте и отражайте обе ID.
    • Fehlertexte: В продакшене — никаких внутренних подробностей. Отладочные детали доступны только контролируемо (Stage/Feature-Flag) и, в случае сомнений, только в логах.

    Оценка: почему RemObjects SDK здесь может иметь преимущество

    В экосистемах Delphi REST-Server часто строят с помощью лёгких фреймворков (например, минималистичных HTTP-роутеров). RemObjects SDK раскрывает свои сильные стороны, когда у вас уже есть или требуется многослойная архитектура:

    • Чёткие границы сервисов: методы сервиса явно определены, контракты поддерживают версионирование.
    • Транспорты и сериализация: Можно использовать JSON, но также и другие форматы сообщений (в зависимости от настройки), не смешивая их с бизнес-логикой.
    • Эксплуатация: Варианты хостинга и интеграция в существующие Windows- и Linux-сервисы планируемы, включая аккуратные развёртывания.

    Показанный подход дополняет это теми элементами, которых в повседневной работе часто не хватает: унифицированные объекты ошибок, детерминированное версионирование и коррелируемое логирование. Особенно для индивидуального корпоративного ПО с длительным жизненным циклом это экономит время при обновлениях и при интеграции внешних систем.

    Вывод: стоит ли затраченные усилия — и где подход перестаёт быть оправданным?

    Польза проявляется, когда ваш интерфейс REST не просто «работает», а остаётся длительно эксплуатируемым: стабильные JSON-контракты, версионирование без URL-хаоса, воспроизводимые ошибки и отладка без угадываний. Именно в этих сценариях подход с Context, Correlation-ID и централизованным Exception-Mapping в RemObjects SDK оказывается эффективным.

    Границы применения: Если у вас только один краткоживущий эндпойнт без интеграционных партнёров, версияция по media-type быстро превращается в overengineering. Также snapshot-логирование имеет смысл лишь при дисциплинированной реализации redaction и механизмов активации. И: если ваш стек прокси «оптимизирует» или удаляет заголовки, сначала приведите инфраструктуру в порядок — иначе вы будете отлаживать не тот уровень.

    Если вы модернизируете существующее серверное окружение Delphi или нужно аккуратно интегрировать процессно-ориентированное решение в ERP/DMS/CRM, именно эти механизмы часто определяют разницу между «работает в тесте» и «работает в эксплуатации».

    В профессиональной среде также важную роль играют Delphi REST-API и REST-сервер и Remobjects Sdk Delphi, когда интеграции, потоки данных и дальнейшая разработка должны слаженно взаимодействовать.

    Обсудить проект или проект по модернизации с Net-Base.

    Следующий шаг

    Если из темы вырастет реальный проект, архитектуру, текущую систему и эксплуатацию следует рассматривать совместно на ранних этапах.

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

    • Текущее состояние, целевое состояние и технические риски оцениваются совместно.
    • REST, доступ к данным, порталы и развертывание не переносятся на более поздние этапы.
    • Вы заранее видите, какой путь экономически и эксплуатационно жизнеспособен.

    Поделиться записью

    Поделиться этой записью напрямую

    LinkedIn, X, XING, Facebook, WhatsApp и E-Mail доступны сразу. Для Instagram мы сразу подготовим ссылку и краткий текст.

    Электронная почта

    Instagram открывается в новой вкладке. Ссылка и короткий текст предварительно копируются в буфер обмена.