Від теми журналу до практики проєкту
Відповідні сторінки послуг і технічні сторінки до публікації
Чому „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‑заголовок.
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-контракту, яка керує поведінкою та полями.
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, який стандартизує процес.
Практичний порядок дій (свідомо коротко й наближено до реалізації)
- Зчитати Correlation-ID з заголовка запиту
X-Correlation-ID; якщо відсутній — згенерувати на сервері (наприклад, GUID). - Зчитати версію контракту з
Accept(або зX-Api-Version). - Протоколювати старт запиту: метод, шлях, Correlation-ID, віддалена IP, запустити вимірювання тривалості.
- Виконати бізнес-логіку; доступи до БД за можливості укласти в транзакції.
- Перехопити Exception: визначити HTTP-статус, створити JSON-об’єкт помилки, встановити Response-Header
X-Correlation-ID. - Протоколювати кінець запиту: статус, тривалість, за потреби код помилки.
Потоки на сервері: чому 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, коли інтеграції, потоки даних і подальший розвиток мають коректно взаємодіяти.
Наступний крок
Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.
Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.
- Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
- REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
- Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.