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-сервіси або за IIS/реверс‑проксі) та визначені місця для централізованої обробки помилок. Але в розвинених корпоративних програмних ландшафтах часто бракує послідовно впровадженого контракту: які поля JSON є стабільними? Як ми сигналізуємо про помилки? Як відновити ідентичність запиту, якщо він пройшов через Load Balancer, TLS-термінацію та кілька шарів бекенда?

Наведений підхід (включно з Delphi-сніпетом) показує надійну лінію для RemObjects SDK: версіонувати JSON‑контракти, примусово вимагати Correlation-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-пуристичним», а радше прагматичним сервісним фреймворком. Саме тому варіант з Media Type вартий уваги: ви зберігаєте стабільні кінцеві точки і водночас можете еволюціонувати контракти. Важливо завжди аналізувати версію централізовано, приймати рішення в одному місці і переносити результат у контекст сервісу.

Коли варіант з Accept‑заголовком дає збій?

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

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

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

    • Correlation-ID: Унікальний ідентифікатор для кожного запиту, який повертається в Response та посилається в логах.
    • Exception-Mapping: Переклад внутрішніх Delphi-Exceptions у стабільні, клієнт-оброблювані об’єкти помилок (вкл. 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.

    Мета: Стабільний контекст запиту замість „irgendwo im Threadlocal“

    Цей фрагмент свідомо розділяє: TApiContext — мінімальний стан, який ви повинні передавати. У RemObjects SDK багато чого працює через контекст сервера/каналу. У гетерогенних проектах (наприклад, додаткові воркер-потоки, DB-Queue, фонові задачі) явне передавання часто більш надійне, ніж неявні Threadlocal’и, оскільки воно робить паралелізм і переключення контексту більш видимими.

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

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

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

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

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

    У RemObjects-проектах часто працюють безпосередньо на рівні окремих методів сервісу. Це спочатку добре масштабується, але на експлуатації перетворюється на проблему: кожен метод будує логування й обробку помилок по-різному. Чітким розмежуванням є Service-Basis або Dispatcher, який стандартизує процес.

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

    1. Зчитати Correlation-ID з заголовка запиту X-Correlation-ID; якщо відсутній — згенерувати на сервері (наприклад, GUID).
    2. Зчитати версію контракту з Accept (або з X-Api-Version).
    3. Протоколювати старт запиту: метод, шлях, Correlation-ID, віддалена IP, запустити вимірювання тривалості.
    4. Виконати бізнес-логіку; доступи до БД за можливості укласти в транзакції.
    5. Перехопити Exception: визначити HTTP-статус, створити JSON-об’єкт помилки, встановити Response-Header X-Correlation-ID.
    6. Протоколювати кінець запиту: статус, тривалість, за потреби код помилки.

    Потоки на сервері: чому Correlation-ID без дисципліни контексту втрачає сенс

    Типовий Delphi-крайній випадок: метод сервісу запускає асинхронну роботу (наприклад, генерацію звітів, імпорт, пуш у DMS). Тоді початковий потік запиту вже не той, що потім пише рядки логу. Якщо Correlation-ID відома лише «на початку», слідування руйнується.

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

    Типові точки перелому в RemObjects-/Delphi-серверах:

    • DB-з’єднання на потік: BDE-Ablosung mit nativer Anbindung-Verbindungen sind nicht automatisch thread-sicher teilbar. Ein Connection-Pool oder pro Thread eine Verbindung ist häufig sinnvoller als „eine globale Connection“.
    • Межі транзакцій: Wenn Sie innerhalb eines Requests mehrere Schritte haben, die zusammengehören, muss die Transaktion in der gleichen logischen Einheit bleiben. Asynchrone Arbeit darf nicht „aus Versehen“ in der gleichen Transaktion weiterlaufen.
    • Скасування: Wenn der Client abbricht (Proxy timeout, Browser closed), läuft der Server oft weiter. Überlegen Sie bewusst, ob Hintergrundarbeit dann noch Sinn ergibt.

    Доступ до даних і коди помилок: 409 ist nicht „auch ein 500“

    У проєктах інтеграції чисте відображення помилок більше, ніж косметика. Es entscheidet, ob ein Gegenüber (ERP-Connector, ETL-Job, портал клієнта) korrekt reagieren kann. Ein paar praxisnahe Leitplanken, die sich in Delphi/RemObjects-Umgebungen bewährt haben:

    • 400 Bad Request: Validierung, fehlende/ungültige Parameter, JSON nicht parsebar. Wichtig: Die Antwort soll stabil bleiben, auch wenn der Body kaputt ist.
    • 401/403: Authentifizierung vs. Berechtigung trennen. 401 bedeutet „keine/ungültige Identität“, 403 „Identität ok, aber verboten“.
    • 404: Ressource existiert nicht. Vorsicht bei Security: Nicht immer verraten, ob etwas existiert.
    • 409 Conflict: Fachlicher Konflikt (z. B. Versionskonflikt, „Status erlaubt diese Aktion nicht“, eindeutige Schlüsselverletzung, wenn sie fachlich relevant ist).
    • 422 Unprocessable Content: Wenn syntaktisch alles ok ist, aber fachliche Validierung scheitert (nicht jedes Team nutzt 422, aber es ist oft klarer als 400).
    • 500: Alles, was Sie nicht sauber klassifizieren können. Dazu gehört auch „DB down“, „Timeout“, „Unhandled Exception“.

    Delphi-специфічний Kniff: Viele DB-Fehler kommen als generische Exceptions hoch. Es lohnt sich, an der Datenzugriffsschicht gezielt auf bekannte Situationen zu prüfen und sie in EApiError zu überführen. Wichtig dabei: Keine SQL-Fragmente oder internen Tabellen-/Spaltennamen in die Client-Message übernehmen. Diese Details gehören ins Log, nicht in die Response.

    Хитрість für Debugging: reproduzierbare Fehler durch „Contract Snapshot“

    Ungewöhnlich, aber im Betrieb extrem hilfreich: Speichern Sie bei Fehlern (oder gezielt bei bestimmten Correlation-IDs) einen „Snapshot“ aus Request-Headern + Request-Body in einer Debug-Spool-Datei. Das ist kein Dauerlogging (Datenschutz/Volumen), sondern ein kontrolliertes Werkzeug, um schwer reproduzierbare Fälle aus Produktionsnähe nachzustellen.

    Wichtig: Ein Snapshot darf niemals ungefiltert Auth-Header, Tokens oder personenbezogene Daten persistieren. In der Praxis bedeutet das: Redaction (Maskierung) und Aktivierung nur über Feature-Flag oder Whitelist (z. B. nur für bestimmte Correlation-IDs, kurze Zeitfenster).

    Saubere Umsetzung in der Praxis: Maskieren statt Weglassen

    In echten Integrationen sind gerade die „kritischen“ Felder oft die, die man zum Debuggen bräuchte (z. B. Identifikatoren). Statt pauschalem Weglassen ist Maskieren besser: Token teilweise ersetzen, E-Mail nur Domain behalten, IBAN nur die letzten Ziffern. So bleibt der Fall reproduzierbar, ohne unnötige Daten im Dateisystem zu verteilen. Zusätzlich sollte der Snapshot klar als Debug-Artefakt gekennzeichnet sein und eine definierte Aufbewahrungszeit haben.

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

    Eine REST API endet selten direkt am Client. Typisch sind Ketten aus Reverse Proxy, TLS-Termination, WAF oder API-Gateway. Daraus ergeben sich praktische Punkte:

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

    Einordnung: Warum RemObjects SDK hier im Vorteil sein kann

    In Delphi-Ökosystemen werden REST-сервери oft mit leichteren Frameworks (z. B. minimalistische HTTP-Router) gebaut. RemObjects SDK spielt seine Stärke aus, wenn Sie bereits eine mehrschichtige Architektur haben oder brauchen:

    • Klare Service-Grenzen: Service-Methoden sind explizit, Contracts sind versionierbar.
    • Transporte und Serialisierung: Sie können JSON sprechen, aber auch andere Message-Formate (je nach Setup), ohne die Fachlogik zu verquirlen.
    • Betrieb: Hosting-Optionen und Integration in bestehende Windows- та Linux-сервіси sind planbar, inklusive sauberer Rollouts.

    Der gezeigte Ansatz ergänzt das um die Teile, die im Alltag oft fehlen: einheitliche Fehlerobjekte, deterministische Versionierung und korrelierbares Logging. Gerade bei individueller Unternehmenssoftware mit langen Lebenszyklen sparen Sie damit Zeit bei Updates und bei der Integration externer Systeme.

    Fazit: Lohnt sich der Aufwand – und wo kippt der Ansatz?

    Der Mehrwert entsteht, wenn Ihre REST-Schnittstelle nicht nur „funktioniert“, sondern dauerhaft betreibbar ist: stabile JSON-Verträge, Versionierung ohne URL-Wildwuchs, nachvollziehbare Fehler und Debugging ohne Ratespiel. Genau dort ist der Ansatz mit Context, Correlation-ID und zentralem Exception-Mapping in RemObjects SDK stark.

    Einsatzgrenzen: Wenn Sie nur einen einzelnen, kurzlebigen Endpunkt ohne Integrationspartner haben, wirkt Media-Type-Versionierung schnell wie Overengineering. Auch Snapshot-Logging ist nur sinnvoll, wenn Sie Redaction und Aktivierung diszipliniert implementieren. Und: Wenn Ihr Proxy-Stack Header „optimiert“ oder entfernt, müssen Sie zuerst die Infrastruktur geradeziehen, sonst debuggen Sie die falsche Schicht.

    Wenn Sie eine bestehende Delphi-серверну інфраструктуру modernisieren oder eine prozessnahe Softwarelösung sauber in ERP/DMS/CRM integrieren müssen, sind genau diese Mechanismen aber häufig der Unterschied zwischen „läuft im Test“ und „läuft im Betrieb“.

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

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

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

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

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

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

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

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

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

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

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