Od tématu magazínu k projektové praxi
Vhodné stránky služeb a technické stránky k příspěvku
Proč se „REST API s RemObjects SDK“ v praxi často rozhoduje v okrajových situacích
Jedna REST API s RemObjects SDK málokdy stojí nebo padá na „Hello World“ servisu, ale na místech, kde se střetává provoz, legacy a integrace: verzování bez odstávek, konzistentní chování při chybách napříč všemi koncovými body, reprodukovatelné ladění v proxy řetězcích a schopnost jednoznačně korelovat požadavky v případě problémů.
RemObjects SDK pro to přináší velkou část infrastruktury: služby, formáty zpráv, serializaci, hosting (např. jako Windows- a Linux-Services nebo za IIS/Reverse Proxy) a definovaná místa pro centrální zpracování chyb. Co ve vyspělých podnikových softwarových krajinách často chybí, je konsekventně vedený kontrakt: Která JSON pole jsou stabilní? Jak signalizujeme chyby? Jak znovu identifikujeme požadavek, když prošel load balancerem, TLS-terminací a několika backend vrstvami?
Následující přístup (včetně Delphi-ukázka) ukazuje robustní linii pro RemObjects SDK: verzování JSON kontraktů, Correlation-ID (ID požadavku pro sledování) vynucovat, překládat výjimky do HTTP stavů a JSON chybových objektů a přitom neladění a provoz stavět proti sobě. Navíc se podíváme na okrajové případy, které v reálných prostředích pravidelně nastávají: threadování na serveru, přístupy k databázi při BDE-ablaci s nativním připojením, proxy hlavičky, time-outy a „špinaté“ klientské payloady.
Architektonické rozhodnutí: verzování přes MIME typ místo URL
Mnoho API verzují přes cesty jako /v1/. To je pragmatické, ale v dlouhodobých integracích (např. napojení ERP/DMS/CRM) to často vede k duplikaci URL, duplicitním routám, duplicitním testům a otázce „Kterou verzi vlastně používáme?“ v provozních příručkách.
Alternativou je verzování přes Media Type (negociace obsahu). Klient například pošle Accept: application/vnd.company.order+json;v=2. Server deterministicky přečte verzi a přizpůsobí chování kontraktu/DTO. Funguje to v proxy a cache řetězcích, pokud se hlavičky správně předávají. Pro administrátory je to navíc dobře kontrolovatelné: požadavek lze v Curl/Postmanu reprodukovat, aniž by se lišily URL.
RemObjects SDK není „REST-puristický“, ale pragmatické servisní framework. Právě proto se varianta s typem média vyplatí: můžete zachovat stabilní koncové body a přitom kontrakty vyvíjet dál. Důležité je, že verzi vždy vyhodnotíte, centrálně na jednom místě rozhodnete a výsledek převezmete do kontextu služby.
Kdy selže varianta založená na Accept-hlavičce?
V praxi existují tři typická místa selhání, která je vhodné předem vyřešit:
- Proxy-Policies: Některé reverzní proxy nebo WAF pravidla normalizují nebo filtrují Accept-hlavičku. Pak vaše API tiše spadne na výchozí nastavení. Řešení: proxy pravidla explicitně zkontrolovat, případně přejít na
X-Api-Versionjako alternativu. - Klientské knihovny: Některé HTTP klientské knihovny nastaví vlastní Accept-hlavičky a přepíší hodnoty. Řešení: podporovat verzi kontraktu i jako volitelný dotazovací parametr (pouze jako fallback), nebo Accept-hlavičku serverově tolerantně parsovat.
- Caching: Pokud se používá cachování odpovědí, musí se cache lišit podle
Accept(Vary: Accept), jinak doručí verzi 1 klientům verze 2. Řešení: explicitně nastavitVarynebo deaktivovat cachování na úrovni API.
Source-Schnipsel: Request-Context, Correlation-ID, Version und Error-Mapping
Kód je záměrně navržen tak, aby se dal integrovat do stávajících projektů serveru RemObjects: malá vrstva kontextu, parser pro verzi API (z Accept), mechanismus Correlation-ID a centrální mapování výjimek. Pojmy:
- Correlation-ID: Jednoznačné ID pro každý požadavek, které se vrací v odpovědi a je referencováno v logech.
- Exception-Mapping: Převod interních Delphi-výjimek na stabilní, klientem zpracovatelné objekty chyb (včetně HTTP statusu).
- Contract-Version: Verze JSON kontraktu, která řídí chování a pole.
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.Účel: stabilní kontext požadavku místo „kdekoliv v threadlocal“
Úryvek záměrně rozděluje: TApiContext je minimální stav, který chcete předávat. V RemObjects SDK probíhá mnoho přes server-/channel-kontext. V heterogenních projektech (např. další worker-thready, DB-queue, background joby) je explicitní předávání často robustnější než implicitní threadlocal, protože tím činíte souběžnost a přepínání kontextu viditelnější.
Okolnosti: Varianta přes Accept-Header předpokládá, že váš reverse proxy (nginx, IIS ARR, Traefik) header předává beze změny. V některých prostředích jsou „neobvyklé“ Accept-headery filtrovány nebo agregovány.
Úskalí: Verzionování přes Accept je tolik dobré, kolik jsou vaše testy. Pokud klienti používají knihovny, které Accept přepisují, může API najednou spadnout na default. Pro legacy klienty má smysl defaultní fallback, ale musí být viditelný v monitoringu (např. log-warning „Version defaulted“).
Varianty: Pokud dáváte přednost verzování přes X-Api-Version: parser je identický, jen zdroj je jiný header. Z pohledu gatewayů je to někdy snazší kontrolovat.
Integrace do RemObjects SDK: Correlation-ID a mapování výjimek při vstupu služby
Skutečný efekt nastane, když mechaniku konzistentně aplikujete na okraji vašeho serveru: jednou na vstupu požadavku číst z headerů, jednou na výstupu výjimek přeložit do stabilní response. Podle hostingu (např. RO-HTTP-Server, IIS-hosting, samostatně provozovaný Windows-/Windows- a Linux-Services) se konkrétní hook-body liší; princip zůstává stejný: postavit context, zavolat business logiku, centrálně mapovat výjimky.
V RemObjects-projektech se často pracuje přímo v rámci jednotlivé servisní metody. To zpočátku škáluje dobře, ale v provozu se to rozbije: každá metoda staví logging a chybovou logiku jinak. Čistým rozhraním je základní třída služby nebo Dispatcher, který standardizuje.
Praktický postup (záměrně krátce a implementačně blízko)
- Correlation-ID přečíst z request-headeru
X-Correlation-ID; pokud chybí, vygenerovat na serveru (např. GUID). - Verzi kontraktu přečíst z
Accept(nebo zX-Api-Version). - Zalogovat start požadavku: metoda, cesta, Correlation-ID, vzdálená IP, zahájit měření trvání.
- Spustit business logiku; DB-přístupy co nejvíce zapouzdřit transakčně.
- Zachytit výjimku: určit HTTP status, vytvořit JSON chybový objekt, nastavit response-header
X-Correlation-ID. - Zalogovat konec požadavku: stav, doba, případně chybový kód.
Threading na serveru: Proč je Correlation-ID bez disciplíny kontextu bezcenná
Častý Delphi okrajový případ: servisní metoda spustí asynchronní práci (např. generování reportu, import, push do DMS). Pak původní request-thread už není ten, který později zapisuje logy. Pokud je Correlation-ID známá jen „na začátku“, dohledatelnost se rozpadne.
Pragmatické pravidlo: Vše, co výslovně neopustí Request-Thread, má být explicitně předáno kontextem. I když to vypadá jako delší seznam parametrů, vyplatí se to. Alternativně lze pracovat s jasně definovaným objektovým kontextem, který je záměrně předán workerům (místo globálních proměnných nebo skrytých singletonů).
Typické kritické body v RemObjects-/Delphi-serverech:
- DB připojení na vlákno: BDE-Ablosung mit nativer Anbindung připojení nejsou automaticky bezpečně sdílitelná mezi vlákny. Pool připojení nebo jedno připojení na vlákno je často smysluplnější než „globální připojení“.
- Hranice transakcí: Pokud máte v rámci jednoho requestu více kroků, které k sobě patří, musí transakce zůstat v té samé logické jednotce. Asynchronní práce nesmí „omylem“ pokračovat v téže transakci.
- Zrušení: Když klient přeruší spojení (timeout proxy, zavřený prohlížeč), server často pokračuje. Zvažte vědomě, zda má v takovém případě smysl pokračovat s pozadní prací.
Přístup k datům a kódy chyb: 409 není „také 500“
V integračních projektech je čisté mapování chyb víc než kosmetika. Rozhoduje o tom, zda protistrana (ERP-Connector, ETL-Job, portál pro zákazníky) může správně reagovat. Několik praktických pravidel, která se osvědčila v Delphi/RemObjects prostředích:
- 400 Bad Request: Validace, chybějící/neplatné parametry, JSON není parsovatelný. Důležité: odpověď by měla zůstat stabilní i když je tělo poškozené.
- 401/403: Oddělte autentizaci a autorizaci. 401 znamená „žádná/neplatná identita“, 403 „identita v pořádku, ale zakázáno“.
- 404: Zdroj neexistuje. Pozor z hlediska bezpečnosti: ne vždy prozrazujte, zda něco existuje.
- 409 Conflict: Doménový konflikt (např. konflikt verzí, „stav tuto akci nedovoluje“, porušení unikátního klíče, pokud je z pohledu domény relevantní).
- 422 Unprocessable Content: Pokud je syntakticky vše v pořádku, ale doménová validace selže (ne každý tým používá 422, ale často je to jasnější než 400).
- 500: Vše, co nelze přesně klasifikovat. Patří sem také „výpadek DB“, „Timeout“, „neobsloužená výjimka“.
Delphi-specifický tip: Mnoho chyb databáze se objeví jako generické výjimky. Vyplatí se na vrstvě přístupu k datům cíleně kontrolovat známé situace a převádět je do EApiError. Důležité: Nezařazujte fragmenty SQL nebo interní názvy tabulek/sloupců do zprávy klientovi. Tyto detaily patří do logu, ne do odpovědi.
Tip pro ladění: reprodukovatelné chyby pomocí „Contract Snapshot“
Neobvyklé, ale v provozu extrémně užitečné: U chyb (nebo selektivně pro určité Correlation-IDs) uložte „snapshot“ z hlaviček požadavku + těla požadavku do debug-spool souboru. Není to trvalé logování (ochrana osobních údajů/objem), ale kontrolovaný nástroj k reprodukci těžko reprodukovatelných případů z produkční blízkosti.
Důležité: Snapshot nikdy nesmí nevyfiltrovaně persistovat autentizační hlavičky, tokeny nebo osobní údaje. V praxi to znamená: Redaction (maskování) a aktivace pouze přes feature-flag nebo whitelist (např. jen pro určité Correlation-IDs, krátké časové okno).
Čisté provedení v praxi: maskování místo vynechání
V reálných integracích jsou právě „kritická“ pole často ta, která byste k ladění potřebovali (např. identifikátory). Místo plošného vynechávání je lepší maskování: částečná náhrada tokenů, u e-mailu ponechat jen doménu, u IBANu jen poslední číslice. Tak zůstane případ reprodukovatelný, aniž by se zbytečně rozšiřovaly údaje v souborovém systému. Navíc by měl být snapshot jasně označen jako debug-artefakt a mít definovanou dobu uchovávání.
Bezpečnost a provoz: předávání hlaviček, řetězce proxy a timeouty
Jedna REST API zřídka končí přímo u klienta. Běžné jsou řetězce s reverse proxy, TLS-terminací, WAF nebo API-gateway. Z toho vyplývají praktické body:
- Remote IP: Nespoléhejte se bez přezkoumání na
X-Forwarded-For. Přijímejte ho jen od důvěryhodných proxy a jinak používejte přímo IP ze socketu. V provozních příručkách by mělo být uvedeno, které Hops jsou „trusted“. - Timeouts: Má-li proxy timeout 30 sekund, ale vaše backend služba potřebuje 2 minuty, vytvoříte ghost-requests. Nastavte timeouty konzistentně podél celé řetězec a rozhodněte se: synchronní request nebo job-pattern (202 Accepted + status endpoint).
- Correlation-ID: Přidávejte Correlation-ID do response-headerů, aby administrátoři mohli sjednotit záznamy z logů a z klientské strany. Pokud gateway používá vlastní Request-IDs: logujte obě ID a mapujte je navzájem.
- Fehlertexte: V produkci žádné interní detaily. Debug-informace pouze kontrolovaně (stage/feature-flag) a v pochybnostech pouze v logu.
Zařazení: Proč může být RemObjects SDK v tomto případě výhodné
V Delphi-ekosystémech se REST-servery často stavějí s lehčími frameworky (např. minimalistické HTTP-routery). RemObjects SDK ukáže své silné stránky, pokud už máte nebo potřebujete vícevrstvou architekturu:
- Jasné hranice služeb: Service-metody jsou explicitní, kontrakty jsou verzovatelné.
- Transporty a serializace: Můžete komunikovat přes JSON, ale i další formáty zpráv (dle nastavení), aniž byste zamazávali aplikační logiku.
- Provoz: Možnosti hostingu a integrace do stávajících Windows- a Linux-servisů lze plánovat, včetně čistých rolloutů.
Návrh uvedený výše doplňuje části, které v každodenním provozu často chybějí: jednotné objekty chyb, deterministická verzování a korrelovatelné logování. Především u individuální podnikové softwarové řešení s dlouhým životním cyklem vám to ušetří čas při updatech a při integraci externích systémů.
Závěr: Vyplatí se ten náklad — a kdy tento přístup přechází v přehnanost?
Hodnota vzniká, pokud vaše REST rozhraní není jen „funkční“, ale dlouhodobě provozovatelné: stabilní JSON-smlouvy, verzování bez divokých URL, srozumitelné chyby a ladění bez hádání. Právě zde je přístup se Context, Correlation-ID a centrálním Exception-Mapping v RemObjects SDK silný.
Hranice použití: Pokud máte pouze jediný, krátkodobý endpoint bez integračních partnerů, může Media-Type-Versionierung rychle působit jako overengineering. Snapshot-Logging má smysl jen pokud disciplinovaně implementujete Redaction a aktivaci. A: pokud váš proxy-stack hlavičky „optimalizuje“ nebo odstraňuje, musíte nejdříve upravit infrastrukturu, jinak budete debugovat špatnou vrstvu.
Pokud modernizujete existující Delphi-serverovou krajinu nebo potřebujete čistě integrovat procesně blízké softwarové řešení do ERP/DMS/CRM, jsou právě tyto mechanismy často rozdílem mezi „běží v testu“ a „běží v provozu“.
V odborném prostředí hrají také Delphi REST-API a REST-server a Remobjects Sdk Delphi důležitou roli, když integrace, datové toky a další rozvoj musí spolu bezproblémově fungovat.
další krok
Když se z tématu stane reálný projekt, měly by být architektura, stávající systém a provoz posuzovány společně již v rané fázi.
Podporujeme nejen při jednotlivých otázkách, ale i v případě, že se z útržků zdrojového kódu, legacy témat nebo nápadů na portál má vyvinout robustní podnikový projekt.
- Současný stav, cílový stav a technická rizika jsou hodnoceny společně.
- REST, přístup k datům, portály a rollout nebudou přesunuty do pozdějších fází.
- Včas zjistíte, která varianta je ekonomicky i provozně životaschopná.