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 mit RemObjects SDK рядко се оценява по „Hello World“ услугата, а по-скоро по местата, където експлоатацията, наследеният код и интеграцията се срещат: версияция без прекъсване, консистентно поведение при грешки за всички крайни точки, възпроизводимо дебъгване при вериги от проксита и способността да се корелират заявките недвусмислено при проблеми.

RemObjects SDK доставя значителна инфраструктура: услуги, формати на съобщения, сериализация, хостинг (напр. като Windows- и Linux-Services или зад IIS/Reverse Proxy) и дефинирани места за централизирано обработване на грешки. Това, което в развиващите се бизнес-софтуерни пейзажи често липсва, е последователно проведен договор: кои JSON-полета са стабилни? Как сигнализираме грешки? Как разпознаваме заявка, след като е минала през Load Balancer, TLS-Termination и няколко backend слоя?

Следваният подход (включително Delphi-фрагменти) показва здрава линия за RemObjects SDK: версиониране на JSON-договорите, налагане на Correlation-ID (Request-ID за проследяване), превеждане на Exceptions в HTTP статус и JSON-обекти за грешки, без да противопоставяме дебъгване и експлоатация. Допълнително разглеждаме крайни случаи, които в реални среди се появяват редовно: многопоточност на сървъра, достъп до бази данни при BDE-замяна с нативна връзка, Proxy-хедъри, таймаути и „нечисти“ клиентски payloads.

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

Много API-та версионират чрез пътища като /v1/. Това е прагматично, но при дългосрочни интеграции (напр. ERP/DMS/CRM връзки) често води до дублиране на URL, дублирани маршрути, дублирани тестове и „Коя версия всъщност използваме?“ в експлоатационните ръководства.

Алтернатива е версиониране чрез Media Type (Content Negotiation). Клиентът изпраща например Accept: application/vnd.company.order+json;v=2. Сървърът детерминистично извлича версията и адаптира поведението на Contract/DTO. Това работи в вериги от проксита и кешове, ако хедърите се препредават коректно. За администраторите е също така лесно проверимо: заявката може да се възпроизведе с Curl/Postman, без да се различават URL адресите.

RemObjects SDK не е „REST-пуристично“, а прагматична сервизна рамка. Именно затова вариантът с медиен тип си струва: можете да запазите стабилни крайни точки и същевременно да развивате договорите. Важно е да оценявате версията винаги, да вземете централизирано решение на едно място и да прехвърлите резултата в контекста на услугата.

Кога се проваля вариантът с Accept-хедъра?

На практика има три типични слаби места, които е добре да адресирате предварително:

  • 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-Serverprojekte: малък слой за контекст, парсър за версията на 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;
    // 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 много работи чрез сървърен/канален контекст. В хетерогенни проекти (напр. допълнителни worker-нишки, DB-опашка, фонoви задачи) експлицитното предаване обаче често е по-устойчиво от имплицитните threadlocal-и, тъй като прави паралелизма и смяната на контекста по-видими.

    Предпоставки: Вариантът с Accept заглавката предполага, че вашият Reverse Proxy (nginx, IIS ARR, Traefik) предава заглавката непроменена. В някои среди „необичайни“ Accept-заглавки се филтрират или агрегират.

    Чести капани: Версионирането чрез Accept е толкова надеждно, колкото и вашите тестове. Ако клиенти използват библиотеки, които презаписват Accept, API може неочаквано да падне на default. За legacy клиенти е разумно да има default-fallback, но това трябва да е видимо в мониторинга (например лог-упреждение „Version defaulted“).

    Варианти: Ако предпочитате да правите версиониране чрез X-Api-Version: парсерът е идентичен, само източникът е друг header. От гледна точка на gateway-и това понякога е по-лесно за контролиране.

    Integration in RemObjects SDK: Correlation-ID und Exception-Mapping am Service-Einstieg

    Реалният ефект възниква, когато приложите механиката последователно по периферията на вашия сървър: веднъж при входа на заявката — да прочетете от заглавките, и веднъж при изхода при изключение — да преведете в стабилен отговор. В зависимост от хостинга (напр. RO-HTTP-Server, IIS-Hosting, самостоятелно оперирани Windows-/Windows- und Linux-Services) конкретните hook-точки се различават; принципът остава същият: изградете Context, извикайте бизнес-логиката, мапнете Exceptions централизирано.

    В RemObjects проекти често се работи директно на ниво метод за услуга. Това първоначално мащабира добре, но при експлоатация се проваля: всеки метод имплементира логване и обработка на грешки по различен начин. Чисто разделение е една базова сервизна реализация или един диспетчер, който стандартизира.

    Практическа последователност (съзнателно кратка и имплементационно насочена)

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

    Threading im Server: Warum Correlation-ID ohne Kontext-Disziplin wertlos wird

    Често срещан Delphi-граничен случай: методът на услугата задейства асинхронна работа (напр. генериране на отчет, импорт, push към DMS). Тогава първоначалната нишка на заявката вече не е същата нишка, която по-късно записва лог редове. Ако Correlation-ID е известна само „в началото“, проследимостта се разпада.

    Прагматично правило: Всичко, което не остава строго в нишката на заявката, трябва да получи контекста експлицитно като параметър. Дори и да изглежда като повече параметри, това се отплаща. Алтернативно можете да работите с ясно дефиниран контекст-обект, който съзнателно се предава на worker-и (вместо глобални променливи или скрити singletons).

    Типични критични точки в RemObjects-/Delphi-сървъри:

    • DB-Connections pro Thread: 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“.
    • Transaktionsgrenzen: 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.
    • Cancellation: Wenn der Client abbricht (Proxy timeout, Browser closed), läuft der Server oft weiter. Überlegen Sie bewusst, ob Hintergrundarbeit dann noch Sinn ergibt.

    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: Всичко, което не можете ясно да класифицирате. Това включва и „DB down“, „Timeout“, „Unhandled Exception“.

    Delphi-специфичен похват: Много DB-грешки излизат като общи Exceptions. Струва си на слоя за достъп до данни целенасочено да проверявате за познати ситуации и да ги превръщате в EApiError. Важно: не включвайте SQL-фрагменти или вътрешни имена на таблици/колони в съобщението към клиента. Тези детайли принадлежат в логовете, не в отговора.

    Debugging-Kniff: reproduzierbare Fehler durch „Contract Snapshot“

    Необичайно, но в експлоатация изключително полезно: при грешки (или целенасочно за определени Correlation-IDs) запазвайте „Snapshot“ от хедърите на заявката + тялото на заявката в debug-spool файл. Това не е постоянно логване (поради защита на данните/обем), а контролирано средство за възпроизвеждане на трудно възпроизводими случаи в близост до продукцията.

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

    Saubere Umsetzung in der Praxis: Maskieren statt Weglassen

    В реални интеграции именно „критичните“ полета често са тези, които ще са нужни за дебъг (напр. идентификатори). Вместо общо премахване, маскирането е по-добро: частична подмяна на токени, запазване само на домейна на имейл, IBAN — само последните цифри. Така случаят остава възпроизводим, без да се разпространяват ненужно данни в файловата система. Допълнително Snapshot-ът трябва да бъде ясно означен като debug артефакт и да има дефиниран срок на задържане.

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

    Една REST API рядко завършва директно при клиента. Обичайни са вериги от reverse proxy, TLS-Termination, WAF или API-Gateway. От това следват практически съображения:

    • Отдалечен IP адрес: Не разчитайте безкритично на X-Forwarded-For. Приемайте го само от доверени проксита и в противен случай използвайте директния socket-IP. В експлоатационните ръководства трябва да е посочено кои хопове са „trusted“.
    • Таймаути: Ако proxy-то има 30 секунди, а вашето бекенд решение се нуждае от 2 минути, ще генерирате ghost-requests. Задайте таймаутите консистентно по веригата и решете: синхронен request или job-патерн (202 Accepted + статусен ендпойнт).
    • Correlation-ID: Включвайте Correlation-ID в response-хедърите, за да могат администраторите да я свързват между логове и клиентска страна. Ако gateway-то ползва собствени request-ID-та: логвайте и двете ID-та и осигурете тяхното съпоставяне.
    • Съобщения за грешки: В продукция не изкарвайте вътрешни детайли. Debug-детайли само контролирано (Stage/Feature-Flag) и при съмнение — само в логовете.

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

    В Delphi-екосистеми REST-сървърите често се изграждат с по-леки фреймуъркове (напр. минималистични HTTP-роутери). RemObjects SDK показва своите предимства, когато вече имате или се нуждаете от многослойна архитектура:

    • Ясно дефинирани граници на услугите: Методите на услугите са експлицитни; контрактите подлежат на версиониране.
    • Транспорт и сериализация: Можете да комуникирате с JSON, но и с други формати на съобщения (в зависимост от конфигурацията), без да смесвате бизнес логиката.
    • Експлоатация: Възможности за хостване и интеграция в съществуващи Windows- и Linux-услуги са предвидими, включително контролирани разгръщания.

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

    Заключение: Струва ли си усилието — и кога подходът става неприложим?

    Добавената стойност се проявява, когато вашият REST интерфейс не просто „работи“, а е дългосрочно експлоатационен: стабилни JSON-договори, версиониране без хаотично разрастване на URL-ите, проследими грешки и отстраняване на проблеми без догадки. Именно там подходът с Context, Correlation-ID и централизирано Exception-Mapping в RemObjects SDK е силен.

    Ограничения на приложимостта: Ако имате само един краткотраен ендпойнт без интеграционни партньори, Media-Type-версионирането бързо може да изглежда като излишно усложнение. Също така Snapshot-логването има смисъл само ако дисциплинирано внедрите Redaction и активиране. И: ако вашият proxy-stack „оптимизира“ или премахва хедъри, първо трябва да приведете инфраструктурата в ред, иначе ще дебъгвате грешния слой.

    Ако модернизирате съществуваща Delphi-сървърна среда или трябва да интегрирате процесно-приложен софтуерен продукт чисто в ERP/DMS/CRM, точно тези механизми често правят разликата между „работи в тест“ и „работи в експлоатация“.

    В предметния контекст също важна роля играят Delphi REST-API и REST-сървър и Remobjects Sdk Delphi, когато интеграциите, потоците от данни и по-нататъшното развитие трябва да взаимодействат безпроблемно.

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

    Следваща стъпка

    Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.

    Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

    • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
    • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
    • Виждате рано кой път е икономически и експлоатационно жизнеспособен.

    Сподели публикацията

    Споделете тази публикация директно

    LinkedIn, X, XING, Facebook, WhatsApp и имейл са незабавно достъпни. За Instagram ще подготвим връзка и кратък текст.

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

    Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.