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 mit RemObjects SDK“ на практиці часто вирішує питання на периферії

Реалізація REST API з RemObjects SDK рідко вирішується на рівні «Hello World»-сервісу; справжній результат визначають ті місця, де експлуатація, успадковані системи (Legacy) та інтеграція стикаються: версіювання без простою, консистентна поведінка при помилках на всіх кінцевих точках, відтворюване налагодження в ланцюгах проксі та здатність однозначно корелювати запити у проблемних випадках.

RemObjects SDK постачає велику інфраструктуру: сервіси, формати повідомлень, серіалізацію, хостинг (наприклад як Windows- und Linux-Services або за IIS/Reverse Proxy) та визначені місця для централізованої обробки помилок. Але в усталених ландшафтах бізнес‑ПЗ часто бракує послідовно реалізованого контракту: які поля JSON є стабільними? Як ми сигналізуємо про помилки? Як повторно ідентифікувати запит, якщо він пройшов через Load Balancer, TLS-термінацію та кілька шарів бекенду?

Наводимий підхід (включно зі Delphi-сніпетом) демонструє надійну лінію для RemObjects SDK: версіювати JSON-контракти, наполягати на Correlation-ID (ID запиту для трасування), перетворювати Exceptions у HTTP-статус та JSON-об’єкти помилок і при цьому не протиставляти налагодження та експлуатацію. Додатково розглядаємо крайові випадки, які в реальних середовищах трапляються регулярно: мультипоточність на сервері, доступи до бази даних при BDE-Ablösung з нативним підключенням, заголовки проксі, таймаути та «брудні» клієнтські payloads.

Architektur-Entscheidung: Versionierung über Medien-Typ statt 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-puristisch», а скоріше прагматичним сервісним фреймворком. Саме тому варіант з media type має сенс: ви можете зберегти стабільні кінцеві точки й одночасно розвивати контракти. Важливо, щоб ви версію завжди зчитували, ухвалювали рішення централізовано в одному місці і передавали результат у контекст сервісу.

Wann kippt die Accept-Header-Variante?

На практиці існує три типові точки відмови, які варто заздалегідь опрацювати:

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

    Код спеціально структурований так, щоб його можна було інтегрувати в існуючі проекти серверів RemObjects: невеликий шар контексту, парсер версії API (із заголовка Accept), механізм Correlation-ID і централізоване відображення виключень. Пояснення термінів:

    • 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;
    // Erwartet z.B.: 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;
    // Im Betrieb keine internen Details, keine SQL, keine Pfade.
    // Für Debug/Stage kann man das über Konfiguration erweitern.
    begin
      if E is EApiError then
        Exit(E.Message);
    
      if E is EArgumentException then
        Exit('Ungültige Parameter.');
    
      Exit('Interner Fehler.');
    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 багато чого працює через контекст сервера/каналу. У гетерогенних проєктах (наприклад, додаткові воркер-потоки, DB-Queue, фонові завдання) явне передавання контексту зазвичай стійкіше, ніж неявні Threadlocal, оскільки воно робить конкурентність і переключення контексту більш помітними.

    Обмеження: Варіант з Accept-заголовком передбачає, що ваш Reverse Proxy (nginx, IIS ARR, Traefik) пропускає заголовок без змін. У деяких середовищах «незвичні» Accept-заголовки фільтруються або об’єднуються.

    Підводні камені: Версіонування через Accept таке ж надійне, як ваші тести. Якщо клієнти використовують бібліотеки, які перезаписують Accept, API може раптово повернутися до значення за замовчуванням. Для застарілих клієнтів корисний дефолтний fallback, але він має бути видимим у моніторингу (наприклад, попередження в логах „Version defaulted“).

    Варіанти: Якщо ви віддаєте перевагу версіонуванню через X-Api-Version: парсер той самий, змінюється лише джерело — інший заголовок. З погляду шлюзів це іноді легше контролювати.

    Інтеграція в RemObjects SDK: Correlation-ID та Exception-Mapping на вході сервісу

    Справжній ефект досягається, якщо ви механіку послідовно застосуєте на краю сервера: одного разу при вході запиту читати з заголовків, одного разу при виході винятку переводити у стабільну відповідь. Залежно від хостингу (наприклад RO-HTTP-Server, IIS-Hosting, власноруч керований Windows-/Windows- та Linux-служби) конкретні точки підключення відрізняються; принцип лишається той самий: побудувати контекст, викликати бізнес-логіку, централізовано відобразити винятки.

    У 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-Connections pro Thread: BDE-Ablosung mit nativer Anbindung-з’єднання не завжди автоматично безпечні для роботи з кількох потоків. Пул з’єднань або по одному з’єднанню на потік часто доцільніший, ніж «одне глобальне з’єднання».
    • Transaktionsgrenzen: Якщо у запиті є кілька кроків, які належать до однієї логічної операції, транзакція має залишатися в межах тієї самої логічної одиниці. Асинхронна робота не повинна «випадково» продовжуватися в тій самій транзакції.
    • Cancellation: Якщо клієнт перериває запит (timeout проксі, закритий браузер), сервер часто продовжує роботу. Усвідомлено вирішіть, чи має фонове виконання тоді сенс.

    Datenzugriff und Fehlercodes: 409 ist nicht „auch ein 500“

    У інтеграційних проєктах чисте відображення помилок важить більше, ніж косметика. Воно визначає, чи може протилежна сторона (ERP-Connector, ETL-job, портал клієнта) коректно реагувати. Кілька практичних орієнтирів, які себе виправдали в середовищах Delphi/RemObjects:

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

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

    Debugging-Kniff: reproduzierbare Fehler durch „Contract Snapshot“

    Незвично, але в експлуатації надзвичайно корисно: зберігайте при помилках (або свідомо для певних Correlation-IDs) «знімок» з заголовків запиту + тіла запиту у файл Debug-Spool. Це не є постійним логуванням (конфіденційність/обсяг), а контрольованим інструментом для відтворення важкорепродукованих випадків поблизу продуктивного середовища.

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

    Saubere Umsetzung in der Praxis: Maskieren statt Weglassen

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

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

    REST API рідко закінчується безпосередньо на клієнті. Типово зустрічаються ланцюги з reverse proxy, TLS-termination, WAF або API-Gateway. З цього випливають практичні аспекти:

    • Remote IP: Не покладайтеся сліпо на X-Forwarded-For. Приймайте його лише від довірених проксі, і в інших випадках використовуйте пряму socket-IP. У експлуатаційних інструкціях має бути зазначено, які хопи вважаються «trusted».
    • Timeouts: Якщо у проксі 30 секунд, а вашому бекенду потрібно 2 хвилини, ви отримаєте Ghost-Requests. Узгоджуйте таймаути по всьому ланцюгу та приймайте рішення: синхронний запит або патерн job (202 Accepted + статусний ендпоінт).
    • Correlation-ID: Включайте Correlation-ID у response-заголовки, щоб адміністратори могли зіставляти його з логами і на боці клієнта. Якщо шлюз використовує власні request-ID — логувати й мапити обидві ідентифікації.
    • Fehlertexte: У продуктивному середовищі без внутрішніх деталей. Деталі відладки лише під контролем (stage/feature-flag) і в сумнівних випадках — тільки в логах.

    Контекст: чому RemObjects SDK тут може мати перевагу

    У Delphi-екосистемах REST-сервери часто будують на легших фреймворках (наприклад мінімалістичних HTTP-роутерах). RemObjects SDK проявляє свої сильні сторони, коли у вас вже є або потрібна багатошарова архітектура:

    • Чіткі межі сервісів: методи сервісу явні, контракти піддаються версіонуванню.
    • Транспорти та серіалізація: ви можете використовувати JSON, але також інші формати повідомлень (залежно від налаштування), не змішуючи предметну логіку.
    • Експлуатація: варіанти розгортання та інтеграція в існуючі Windows- та Linux-сервіси плануються, включно з акуратними rollouts.

    Показаний підхід доповнює це тими елементами, яких у повсякденній роботі часто бракує: уніфіковані об’єкти помилок, детерміноване версіонування і корельоване логування. Особливо для індивідуального корпоративного ПЗ з довгими життєвими циклами це заощаджує час під час оновлень і при інтеграції зовнішніх систем.

    Висновок: чи виправдані зусилля — і коли підхід не підходить?

    Додана цінність виникає, коли ваша REST-інтерфейс не просто «працює», а довгостроково експлуатується: стабільні JSON-контракти, версіонування без хаотичних URL, зрозумілі помилки і відлагодження без здогадок. Саме в цьому місці підхід з Context, Correlation-ID і централізованим Exception-Mapping у RemObjects SDK сильний.

    Межі застосування: Якщо у вас лише один короткочасний ендпоінт без інтеграційних партнерів, медиа-тайп-версіонування швидко стане overengineering. Також snapshot-логування має сенс лише за умови дисциплінованої реалізації redaction і активації. І: якщо ваш проксі-стек «оптимізує» або видаляє заголовки, спочатку доведіть інфраструктуру до ладу, інакше ви відлагоджуватимете не той шар.

    Якщо ви модернізуєте існуючий Delphi-ландшафт серверів або потрібно коректно інтегрувати процесно-орієнтоване рішення в ERP/DMS/CRM, саме ці механізми часто відрізняють «працює в тесті» від «працює в експлуатації».

    У фаховому середовищі також відіграють важливу роль Delphi REST-API und REST-Server та Remobjects Sdk Delphi, коли інтеграції, потоки даних і подальший розвиток повинні взаємодіяти узгоджено.

    Обговорити проєкт або ініціативу модернізації з Net-Base.

    Наступний крок

    Якщо тема перетворюється на реальний проєкт, архітектуру, наявні системи та експлуатацію слід розглядати разом на ранньому етапі.

    Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

    • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
    • REST, доступ до даних, портали та Rollout не відсуваються на пізніший етап.
    • Ви заздалегідь бачите, який шлях є економічно та операційно життєздатним.

    Поділитися дописом

    Поділитися цим дописом безпосередньо

    LinkedIn, X, XING, Facebook, WhatsApp та E‑Mail доступні негайно. Для Instagram ми безпосередньо готуємо посилання та короткий текст.

    Електронна пошта

    Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.