Od tematu magazynowego do praktyki projektowej
Pasujące strony usługowe i techniczne do artykułu
Dlaczego „REST API z RemObjects SDK” w praktyce często rozstrzyga się na obrzeżach
Rzadko REST API z RemObjects SDK ocenia się po „Hello World” — decydują miejsca, w których operacje, rozwiązania legacy i integracja się ścierają: wersjonowanie bez przestojów, jednolite zachowanie w przypadku błędów na wszystkich endpointach, odtwarzalne debugowanie w łańcuchach proxy oraz zdolność jednoznacznego skorelowania żądań w sytuacjach awaryjnych.
RemObjects SDK dostarcza wiele podstawowej infrastruktury: serwisy, formaty komunikatów, serializację, hosting (np. jako Windows- i Linux-Services lub za IIS/Reverse Proxy) oraz wyraźne miejsca do centralnego obsługiwania błędów. W rozrastających się środowiskach biznesowego oprogramowania często brakuje jednak konsekwentnie wdrożonego kontraktu: które pola JSON są stabilne? Jak sygnalizujemy błędy? Jak rozpoznać żądanie, które przeszło przez Load Balancer, TLS-termination i wiele warstw backendu?
Następujące podejście (wraz ze Delphi-snipami) przedstawia wyraźną i odporną linię dla RemObjects SDK: wersjonowanie kontraktów JSON, wymuszenie Correlation-ID (Request-ID do śledzenia), tłumaczenie Exceptions na statusy HTTP i obiekty błędów JSON przy jednoczesnym niezestawianiu debugowania przeciw operacji. Dodatkowo omawiamy przypadki brzegowe, które w rzeczywistych środowiskach występują regularnie: wielowątkowość serwera, dostęp do bazy danych przy BDE-Ablösung z natywnym podłączeniem, nagłówki proxy, timeouty oraz „brudne” payloady klientów.
Decyzja architektoniczna: wersjonowanie przez Media Type zamiast URL
Wiele API wersjonuje się przez ścieżki typu /v1/. To pragmatyczne, ale w długotrwałych integracjach (np. przy podłączeniach ERP/DMS/CRM) często prowadzi do duplikacji URL, podwójnych tras, podwójnych testów i pytania „Której wersji właściwie używamy?” w dokumentacji operacyjnej.
Alternatywą jest wersjonowanie przez Media Type (Content Negotiation). Klient wysyła np. Accept: application/vnd.company.order+json;v=2. Serwer odczytuje wersję deterministycznie i dostosowuje zachowanie kontraktu/DTO. Działa to w łańcuchach proxy i cache, jeśli nagłówki są poprawnie przekazywane. Dla administratorów jest to też dobrze weryfikowalne: żądanie można odtworzyć przez Curl/Postman bez zmiany URL.
RemObjects SDK nie jest „REST-purystyczny”, lecz pragmatycznym frameworkiem serwisowym. Dlatego wariant oparty na typie mediów ma sens: można zachować stabilne endpointy i jednocześnie rozwijać kontrakty. Ważne jest, aby wersję zawsze analizować, decydować centralnie w jednym miejscu i przekazywać wynik do kontekstu serwisu.
Kiedy wariant oparty na nagłówku Accept zawodzi?
W praktyce istnieją trzy typowe punkty krytyczne, które warto zaadresować uprzednio:
- Proxy-Policies: Niektóre Reverse Proxies/reguły WAF normalizują lub filtrują nagłówek Accept. W takim wypadku API cicho wraca do wartości domyślnej. Rozwiązanie: sprawdzić reguły proxy eksplicytnie, ewentualnie użyć pola
X-Api-Versionjako obejścia. - Client-Libraries: Niektóre biblioteki HTTP ustawiają własne nagłówki Accept i nadpisują wartości. Rozwiązanie: wspierać wersję kontraktu też jako opcjonalny parametr query (tylko jako fallback) lub tolerancyjnie parsować Accept po stronie serwera.
- Caching: Wenn Response-Caching im Spiel ist, muss der Cache nach
Acceptvariieren (Vary: Accept), sonst liefert er Version 1 an Version-2-Clients. Lösung:Varybewusst setzen, oder Caching auf API-Ebene deaktivieren.
Fragment źródłowy: Request-Context, Correlation-ID, wersja i mapowanie błędów
Kod jest zaprojektowany tak, aby można go było zintegrować z istniejącymi projektami serwerowymi RemObjects: niewielka warstwa kontekstu, parser wersji API (z nagłówka Accept), mechanizm Correlation-ID oraz centralne mapowanie wyjątków. Pojęcia:
- Correlation-ID: Unikalny identyfikator dla żądania, który pojawia się również w odpowiedzi i jest odwoływany w logach.
- Exception-Mapping: Tłumaczenie wewnętrznych Delphi-wyjątków na stabilne, przez klienta przetwarzalne obiekty błędów (w tym status HTTP).
- Contract-Version: Wersja kontraktu JSON, która steruje zachowaniem i polami.
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.Cel: Stabilny kontekst żądania zamiast „gdzieś w threadlocal”
Fragment celowo rozdziela: TApiContext to minimalny stan, który należy przekazywać. W RemObjects SDK wiele odbywa się w kontekście serwera/kanalu. W heterogenicznych projektach (np. dodatkowe wątki robocze, kolejka DB, zadania w tle) jawne przekazywanie kontekstu jest często bardziej odporne niż ukryte threadlocale, ponieważ sprawia, że współbieżność i zmiany kontekstu są bardziej przejrzyste.
Warunki: Wariant z nagłówkiem Accept zakłada, że reverse proxy (nginx, IIS ARR, Traefik) przekazuje nagłówek bez zmian. W niektórych środowiskach „nietypowe” nagłówki Accept są filtrowane lub scalane.
Pułapki: Wersjonowanie przez Accept jest tak dobre, jak Twoje testy. Jeśli biblioteki klienckie nadpisują Accept, API może niespodziewanie wrócić do wartości domyślnej. Dla klientów legacy sensowny jest domyślny fallback, ale musi być widoczny w monitoringu (np. ostrzeżenie w logu „Version defaulted”).
Alternatywy: Jeśli wolisz wersjonowanie przez X-Api-Version: parser jest identyczny, zmienia się tylko źródło — inny nagłówek. Z punktu widzenia gatewayów czasem łatwiej to kontrolować.
Integracja w RemObjects SDK: Correlation-ID i mapowanie wyjątków na wejściu serwisu
Rzeczywisty efekt uzyskuje się, gdy mechanikę stosuje się konsekwentnie na brzegu serwera: raz przy wejściu żądania odczytać z nagłówków, raz przy wyjściu wyjątków przetłumaczyć na stabilną odpowiedź. W zależności od hostingu (np. RO-HTTP-Server, IIS-Hosting, samodzielnie uruchamiane Windows-/Windows- i Linux-Services) konkretne punkty podczepienia będą się różnić; zasada pozostaje ta sama: zbudować kontekst, wywołać logikę biznesową, centralnie zmapować wyjątki.
W projektach RemObjects często pracuje się bezpośrednio w ramach pojedynczej metody serwisowej. Na początku to dobrze skaluje, ale w eksploatacji się psuje: każda metoda buduje logowanie i obsługę błędów inaczej. Czystym rozwiązaniem jest podstawa serwisu lub dispatcher, który to standaryzuje.
Praktyczny przebieg (świadomie krótko i zorientowany implementacyjnie)
- Odczytać Correlation-ID z nagłówka żądania
X-Correlation-ID; jeśli brak, wygenerować po stronie serwera (np. GUID). - Odczytać wersję kontraktu z
Accept(lub zX-Api-Version). - Zalogować start żądania: metoda, ścieżka, Correlation-ID, IP klienta, rozpocząć pomiar czasu.
- Wykonać logikę biznesową; dostępy do DB otoczyć — o ile to możliwe — transakcją.
- Przechwycić wyjątek: określić status HTTP, utworzyć JSON-obiekt błędu, ustawić w nagłówku odpowiedzi
X-Correlation-ID. - Zalogować koniec żądania: status, czas trwania, ew. kod błędu.
Wielowątkowość na serwerze: dlaczego Correlation-ID bez dyscypliny kontekstu jest bezwartościowy
Częsty Delphi przypadek brzegowy: metoda serwisowa uruchamia pracę asynchroniczną (np. generowanie raportu, import, push do DMS). Wtedy pierwotny wątek żądania nie jest tym samym wątkiem, który później zapisuje wpisy w logu. Jeśli Correlation-ID jest znana tylko „na początku”, śledzenie ulega rozproszeniu.
Pragmatyczna zasada: wszystko, co nie pozostaje ściśle w wątku żądania, powinno otrzymać kontekst przekazany jawnie. Nawet jeśli oznacza to dłuższe listy parametrów, opłaca się to. Alternatywnie można użyć jasno zdefiniowanego obiektu kontekstowego, który świadomie przekazuje się do workerów (zamiast globalnych zmiennych lub ukrytych singletonów).
Typowe punkty krytyczne w serwerach RemObjects-/Delphi:
- Połączenia DB na wątek: BDE-Ablosung mit nativer Anbindung-połączenia nie są automatycznie bezpiecznie współdzielone między wątkami. Pool połączeń lub jedno połączenie na wątek jest często sensowniejsze niż „globalne połączenie”.
- Granice transakcji: Jeśli w ramach jednego requestu występuje kilka kroków należących do tej samej operacji, transakcja musi pozostać w tej samej logicznej jednostce. Prace asynchroniczne nie mogą „przypadkowo” kontynuować się w tej samej transakcji.
- Anulowanie (Cancellation): Gdy klient przerywa połączenie (timeout proxy, zamknięcie przeglądarki), serwer często działa dalej. Zastanów się świadomie, czy praca w tle ma wtedy sens.
Dostęp do danych i kody błędów: 409 to nie „też 500”
W projektach integracyjnych uporządkowane mapowanie błędów to więcej niż kosmetyka. Decyduje o tym, czy partner (ERP-Connector, ETL-Job, portal klienta) może poprawnie zareagować. Kilka praktycznych wytycznych, które sprawdziły się w środowiskach Delphi/RemObjects:
- 400 Bad Request: walidacja, brakujące/nieprawidłowe parametry, JSON nieparsowalny. Ważne: odpowiedź powinna być stabilna, nawet jeśli treść żądania jest uszkodzona.
- 401/403: Rozdziel uwierzytelnianie i autoryzację. 401 oznacza „brak/nieprawidłowa tożsamość”, 403 „tożsamość ok, ale zabronione”.
- 404: Zasób nie istnieje. Uwaga w kontekście bezpieczeństwa: nie zawsze ujawniać, czy coś istnieje.
- 409 Conflict: konflikt biznesowy (np. konflikt wersji, „status nie pozwala na tę akcję”, naruszenie unikatowego klucza, jeśli ma znaczenie biznesowe).
- 422 Unprocessable Content: gdy składnia jest poprawna, lecz walidacja biznesowa nie przechodzi (nie każdy zespół używa 422, ale często jest to jaśniejsze niż 400).
- 500: wszystko, czego nie da się jednoznacznie sklasyfikować. Należą do tego także „DB down”, „Timeout”, „Unhandled Exception”.
Trik specyficzny dla Delphi: wiele błędów DB pojawia się jako genericzne wyjątki. Warto w warstwie dostępu do danych specjalnie rozpoznawać znane sytuacje i przekładać je na EApiError. Ważne: nie przenosić fragmentów SQL ani nazw wewnętrznych tabel/kolumn do komunikatu dla klienta. Te szczegóły należą do logu, nie do odpowiedzi.
Sposób debugowania: odtwarzalne błędy dzięki „Contract Snapshot”
Nietypowe, ale w eksploatacji niezwykle pomocne: zapisz przy błędach (lub celowo dla określonych Correlation-IDs) „snapshot” z nagłówków żądania + treści żądania do pliku debug-spool. To nie jest stałe logowanie (ochrona danych/objętość), lecz kontrolowane narzędzie do odtwarzania trudnych do reprodukcji przypadków blisko produkcji.
Ważne: snapshot nigdy nie powinien bez filtrowania zapisywać nagłówków autoryzacji, tokenów ani danych osobowych. W praktyce oznacza to: redakcję (maskowanie) i włączanie tylko przez feature-flag lub whitelistę (np. tylko dla określonych Correlation-IDs, w krótkich oknach czasowych).
Czyste wdrożenie w praktyce: maskowanie zamiast pomijania
W prawdziwych integracjach „krytyczne” pola często są tymi, które potrzebne są do debugowania (np. identyfikatory). Zamiast ogólnego pomijania lepsze jest maskowanie: częściowa zamiana tokenów, pozostawienie w adresie e-mail tylko domeny, IBAN tylko z ostatnimi cyframi. Dzięki temu przypadek pozostaje odtwarzalny, bez rozprzestrzeniania niepotrzebnych danych w systemie plików. Dodatkowo snapshot powinien być wyraźnie oznaczony jako artefakt debugowy i mieć określony okres przechowywania.
Bezpieczeństwo i eksploatacja: przekazywanie nagłówków, łańcuchy proxy i limity czasu
Interfejs REST rzadko kończy się bezpośrednio przy kliencie. Typowe są łańcuchy z reverse proxy, TLS-termination, WAF lub API-Gateway. Stąd wynikają praktyczne uwagi:
- Zdalne IP: Nie polegaj ślepo na
X-Forwarded-For. Przyjmuj je tylko od zaufanych proxy, w przeciwnym razie użyj bezpośredniego adresu gniazda. W podręcznikach operacyjnych powinno być określone, które hopy są „trusted”. - Limity czasu: Jeśli proxy ma 30 sekund, a Twoje backendy potrzebują 2 minut, wygenerujesz żądania widmo. Ustal limity czasu spójnie wzdłuż całego łańcucha i podejmij decyzję: synchroniczne żądanie czy wzorzec job (202 Accepted + punkt końcowy statusu).
- Correlation-ID: Umieszczaj Correlation-ID w nagłówkach odpowiedzi, aby administratorzy mogli je powiązać z logami i po stronie klienta. Jeśli gateway używa własnych Request-ID: loguj i odwzorowuj obie ID.
- Teksty błędów: W środowisku produkcyjnym nie ujawniaj szczegółów wewnętrznych. Szczegóły debugowania tylko kontrolowanie (Stage / Feature-Flag) i w razie wątpliwości tylko w logu.
Kontekst: Dlaczego RemObjects SDK może tutaj mieć przewagę
W ekosystemach Delphi serwery REST-Server są często budowane przy użyciu lżejszych frameworków (np. minimalistyczne routery HTTP). RemObjects SDK ujawnia swoje zalety, gdy już masz lub potrzebujesz architektury wielowarstwowej:
- Jasne granice usług: Metody serwisowe są jawne, kontrakty można wersjonować.
- Transporty i serializacja: Możesz używać JSON, ale też innych formatów wiadomości (w zależności od konfiguracji), bez mieszania logiki domenowej z warstwą transportu.
- Eksploatacja: Opcje hostingu i integracja z istniejącymi Windows- i Linux-Services są możliwe do zaplanowania, łącznie z uporządkowanymi rolloutami.
Przedstawione podejście uzupełnia to o elementy, których często brakuje w praktyce: jednolite obiekty błędów, deterministyczne wersjonowanie i logowanie umożliwiające korelację. W szczególności przy indywidualnym oprogramowaniu korporacyjnym o długim cyklu życia oszczędza to czas przy aktualizacjach i integracji systemów zewnętrznych.
Wniosek: Czy wysiłek się opłaca — i kiedy podejście przestaje być sensowne?
Wartość dodana pojawia się, gdy Twoje REST-API nie tylko „działa”, lecz jest trwałe w eksploatacji: stabilne kontrakty JSON, wersjonowanie bez rozrostu URL, zrozumiałe błędy i debugowanie bez zgadywania. W tych obszarach podejście z kontekstem, Correlation-ID i centralnym mapowaniem wyjątków w RemObjects SDK jest szczególnie silne.
Granice zastosowania: Jeśli masz tylko pojedynczy, krótkotrwały endpoint bez partnerów integracyjnych, wersjonowanie przez Media-Type szybko stanie się przewymiarowaniem. Logowanie snapshotów ma sens tylko, jeśli konsekwentnie wdrożysz maskowanie (redaction) i mechanizmy aktywacji. I: jeśli Twój stos proxy „optymalizuje” lub usuwa nagłówki, najpierw musisz uporządkować infrastrukturę — inaczej będziesz debugować niewłaściwą warstwę.
Jeżeli modernizujesz istniejącą infrastrukturę serwerową Delphi lub musisz czysto zintegrować rozwiązanie procesowe z ERP/DMS/CRM, to właśnie te mechanizmy często decydują o różnicy między „działa w teście” a „działa w produkcji”.
W obszarze merytorycznym ważną rolę odgrywają także Delphi REST-API i REST-serwer oraz Remobjects Sdk Delphi, gdy integracje, przepływy danych i dalszy rozwój muszą współdziałać w sposób uporządkowany.
Następny krok
Gdy temat stanie się rzeczywistym projektem, architekturę, stan istniejący i eksploatację należy wcześnie rozpatrywać wspólnie.
Wspieramy nie tylko w pojedynczych zagadnieniach, lecz także wtedy, gdy z fragmentów kodu źródłowego, kwestii związanych z systemami legacy lub koncepcji portalu ma powstać solidny projekt dla przedsiębiorstwa.
- Stan istniejący, obraz docelowy i ryzyka techniczne są oceniane łącznie.
- REST, dostęp do danych, portale i Rollout nie są odkładane na później.
- Wcześnie widzą Państwo, która droga jest ekonomicznie opłacalna i operacyjnie wykonalna.