Nuo žurnalo temos iki projekto įgyvendinimo
Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui
Kodėl „REST API su RemObjects SDK“ praktiškai dažnai lemia sprendimus ribinėse situacijose
Vienas REST API su RemObjects SDK retai „sugriūna“ dėl „Hello World“ serviso — sprendimai dažniau priimami tose vietose, kur susiduria eksploatacija, Legacy ir integracija: versijavimas be sustojimų, nuoseklus klaidų elgesys visuose galiniuose taškuose, pakartotinas derinimas proxy-grandinėse ir gebėjimas užklausas problemos atveju vienareikšmiškai susieti.
RemObjects SDK suteikia daug infrastruktūros: servisus, žinučių formatus, serializaciją, hostingą (pvz. kaip Windows- ir Linux-Services arba už IIS/Reverse Proxy) ir apibrėžtas vietas centralizuotam klaidų tvarkymui. Visgi augančiose verslo programinės įrangos kraštovaizdžiose dažnai trūksta konsekiškai taikomo kontrakto: kurie JSON laukai yra stabilūs? Kaip signalizuojame klaidas? Kaip atpažinti užklausą, jei ji praėjo per Load Balancer, TLS-terminaciją ir kelis backend sluoksnius?
Žemiau pateiktas požiūris (įskaitant Delphi-kodo fragmentą) apibrėžia tvirtą liniją RemObjects SDK: versijuoti JSON kontraktus, Correlation-ID (užklausos ID sekimui) privalomai reikalauti, Exceptions konvertuoti į HTTP būsenos kodus ir JSON klaidų objektus ir tuo pačiu neskirti derinimo ir eksploatacijos kaip priešingų tikslų. Papildomai aptariame ribines situacijas, kurios realiose aplinkose pasitaiko reguliariai: daugiagijystė serveryje, duomenų bazės prieigos su BDE-pakeitimu su natyviu prijungimu, proxy-antraštės, timeout’ai ir „nešvarios“ kliento užklausų duomenų struktūros.
Architektūros sprendimas: versijavimas per medijos tipą, o ne per URL
Daugelis API versijuoja per kelius, pvz. /v1/. Tai pragmatiška, tačiau ilgalaikėse integracijose (pvz. ERP/DMS/CRM sujungimuose) tai dažnai sukelia URL dublikavimą, dvigubas maršrutus, dvigubus testus ir operacijos vadovuose užrašytą klausimą „Kurią versiją iš tikrųjų naudojame?“
Alternatyva — versijavimas per Media Type (turinio derinimas). Klientas siunčia pvz. Accept: application/vnd.company.order+json;v=2. Serveris deterministiškai nuskaito versiją ir pritaiko Contract/DTO elgseną. Tai veikia proxy- ir cache-grandinėse, jei antraštės yra tinkamai perduotos. Administratoriams tai taip pat lengvai patikrinama: užklausą galima reproducuoti per Curl/Postman be URL skirtumų.
RemObjects SDK nėra „REST-puristiškas“, o pragmatiškas paslaugų karkasas. Būtent dėl to medijos tipo variantas dažnai apsimoka: galite išlaikyti stabilias galines vietas ir tuo pačiu vystyti kontraktus. Svarbu, kad versiją visada įvertintumėte, centralizuotai priimtumėte sprendimą ir rezultatą perkeltumėte į savo paslaugos kontekstą.
Kada Accept-Header sprendimas gali nepasisekti?
Praktikoje yra trys tipinės gedimo vietos, kurias verta išspręsti iš anksto:
- Proxy politikos: Kai kurios Reverse Proxies arba WAF taisyklės normalizuoja arba filtruoja Accept antraštes. Tokiu atveju jūsų API tyliai grįžta į numatytąją versiją. Sprendimas: aiškiai patikrinti proxy taisykles, esant reikalui persimesti ant
X-Api-Version. - Klientų bibliotekos: Kai kurios HTTP kliento bibliotekos nustato savo Accept antraštes ir perrašo turinį. Sprendimas: palaikyti kontrakto versiją taip pat kaip neprivalomą užklausos parametrą (tik kaip fallback) arba serverio pusėje tolerantiškai parsinti Accept antraštę.
Accept (Vary: Accept), kitaip ji pateiks 1 versiją 2-os versijos klientams. Sprendimas: sąmoningai nustatyti Vary arba išjungti talpinimą API lygyje.Kodo šnipelis: Request-Context, Correlation-ID, Version ir Error-Mapping
Kodas paruoštas taip, kad jį būtų galima įtraukti į esamus RemObjects serverio projektus: nedidelis konteksto sluoksnis, parseris API versijai (iš Accept), Correlation-ID mechanizmas ir centrinis Exception-Mapping. Terminai:
- Correlation-ID: Unikalus identifikatorius kiekvienai užklausai, kuris atsiranda atsakyme ir nurodomas žurnaluose.
- Exception-Mapping: Delphi-išimčių vertimas į stabilius, klientui apdoroti tinkamus klaidų objektus (įskaitant HTTP statusą).
- Contract-Version: JSON sutarties versija, kuri reguliuoja elgesį ir laukus.
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.Tikslas: stabilus užklausos kontekstas, o ne „kur nors Threadlocal“
Kodo fragmentas sąmoningai atskiria: TApiContext yra minimalus būsena, kurią norite perduoti. RemObjects SDK daug kas veikia per serverio-/kanalo kontekstą. Heterogeniniuose projektuose (pvz., papildomi worker-threadai, DB-eilės, foniniai darbai) aiškus konteksto perdavimas dažnai yra patikimesnis už implicitinius Threadlocals, nes taip lygiagretiškumas ir konteksto perjungimai tampa matomesni.
Ribos sąlygos: Accept-Header-variantas reikalauja, kad jūsų reverse proxy (nginx, IIS ARR, Traefik) antraštę perduotų nepakitusią. Kai kuriose aplinkose „neįprastos“ Accept-antraštės yra filtruojamos arba sujungiamos.
Grozio akmenys: Versijavimas per Accept yra tiek pat geras, kiek ir jūsų testai. Jei klientai naudoja bibliotekas, kurios perrašo Accept, API gali netikėtai grįžti prie numatytosios versijos. Legacy klientams numatytas fallback yra prasmingas, tačiau jis turi būti matomas stebėjime (pvz., žurnalo įspėjimas „Version defaulted“).
Variantai: Jei versijavimą verčiau vykdyti per X-Api-Version: parseris yra identiškas, tik šaltinis yra kita antraštė. Iš gateway perspektyvos tai kartais lengviau kontroliuoti.
Integracija į RemObjects SDK: Correlation-ID ir Exception-Mapping prie paslaugos įėjimo
Tikroji nauda atsiranda, kai mechaniką taikote nuosekliai prie serverio krašto: vienąkart prie užklausos įėjimo nuskaityti iš antraščių, vienąkart prie išimčių išeigos konvertuoti į stabilų atsakymą. Priklausomai nuo hosting’o (pvz., RO-HTTP-Server, IIS-Hosting, paties valdomi Windows-/Windows- ir Linux-Services) konkrečios prijungimo vietos skirsis; principas lieka tas pats: sukurti kontekstą, iškviesti verslo logiką, centralizuotai map’inti Exceptions.
RemObjects projektuose dažnai dirbama tiesiogiai prie kiekvienos serviso metodo. Iš pradžių tai gerai skalė, bet eksploatacijoje prasideda problemos: kiekvienas metodas skirtingai sprendžia logging’ą ir klaidų valdymą. Aiškus atskyrimas yra serviso bazė arba dispatcher, kuris standartizuoja.
Praktinis procesas (sąmoningai trumpai ir įgyvendinimui artimas)
- Nuskaityti Correlation-ID iš užklausos antraštės
X-Correlation-ID; jei trūksta, sukurti serverio pusėje (pvz., GUID). - Nuskaityti contract-versiją iš
Accept(arba išX-Api-Version). - Užregistruoti užklausos pradžią: metodas, kelias, Correlation-ID, nuotolinė IP, pradėti trukmės matavimą.
- Vykdyti verslo logiką; DB prieigas, kiek įmanoma, kapsuliuoti transakciškai.
- Pagauti Exception: nustatyti HTTP statusą, sugeneruoti JSON klaidos objektą, nustatyti Response-antraštę
X-Correlation-ID. - Užregistruoti užklausos pabaigą: statusas, trukmė, galimas klaidos kodas.
Threading serveryje: kodėl Correlation-ID be konteksto disciplinos netenka prasmės
Viena dažna Delphi ribinė situacija: serviso metodas inicijuoja asinchroninį darbą (pvz., ataskaitos generavimas, importas, push į DMS). Tada pirminis request-thread nebėra tas, kuris vėliau rašys žurnalo įrašus. Jei Correlation-ID žinoma tik „pradžioje“, sekamumas suyra.
Pragmatiška taisyklė: viskas, kas nelieka griežtai request-threade, turi gauti kontekstą aiškiai perduodamą. Net jei tai atrodo kaip ilgesnės parametrų eilutės, tai atsiperka. Alternatyva — dirbti su aiškiai apibrėžtu konteksto objektu, kuris sąmoningai perduodamas worker’iui (vietoje globalių kintamųjų ar paslėptų singletonų).
Tipiniai kritiniai taškai RemObjects-/Delphi serveriuose:
- DB-Connections pro Thread: BDE-Ablosung mit nativer Anbindung-sujungimai nėra automatiškai saugūs daugiasriegiškume. Connection pool arba kiekvienai gijai atskira jungtis dažnai yra prasmingesnis sprendimas nei „viena globali jungtis“.
- Transaktionsgrenzen: Jei per užklausą atliekate kelis susijusius žingsnius, transakcija turi likti toje pačioje logiškoje vienetėje. Asinchroninis darbas neturi „netyčia“ tęstis toje pačioje transakcijoje.
- Cancellation: Kai klientas nutraukia ryšį (proxy timeout, naršyklė uždaryta), serveris dažnai tęsia darbą. Sąmoningai apsvarstykite, ar fono darbai tokiu atveju dar turi prasmę.
Datenzugriff und Fehlercodes: 409 ist nicht „auch ein 500“
Integracijos projektuose tvarkingas klaidų atvaizdavimas (error mapping) yra daugiau nei kosmetika. Nuo to priklauso, ar pašalinis komponentas (ERP-connector, ETL-job, Kundenportal) gali tinkamai reaguoti. Keletas praktinių gairių, kurios pasiteisino Delphi/RemObjects aplinkose:
- 400 Bad Request: Validacija, trūkstami/neteisingi parametrai, JSON neparseinamas. Svarbu: atsakymas turi būti stabilus net jei body yra sugadintas.
- 401/403: Atskirkite autentifikaciją nuo autorizacijos. 401 reiškia „nėra/neteisinga tapatybė“, 403 – „tapatybė tvarkinga, bet veiksmas uždraustas“.
- 404: Išteklius neegzistuoja. Atsargiai dėl saugumo: ne visada verta atskleisti, ar kažkas egzistuoja.
- 409 Conflict: Domain konfliktas (pvz., versijų konfliktas, „būsena neleidžia šios operacijos“, unikalumo raktų pažeidimas, jeigu tai turi reikšmę verslo logikai).
- 422 Unprocessable Content: Jei sintaksė tvarkinga, bet nepavyksta verslo validacija (ne visi komandos naudoja 422, bet dažnai jis aiškesnis už 400).
- 500: Viskas, ko negalite aiškiai klasifikuoti. Tai apima ir „DB down“, „timeout“, „Unhandled Exception“.
Delphi-specifinė gudrybė: daugelis DB klaidų kyla kaip generinės išimtys. Vertinga duomenų prieigos sluoksnyje tiksliai patikrinti žinomus atvejus ir perkelti juos į EApiError. Svarbu: neperkelkite SQL fragmentų ar vidinių lentelių/stulpelių pavadinimų į kliento žinutę. Šios detalės turi būti loguose, o ne Response.
Debugging-Kniff: reproduzierbare Fehler durch „Contract Snapshot“
Neįprasta, bet eksploatacijoje labai naudinga: klaidos atveju (arba selektyviai pagal tam tikras Correlation-IDs) išsaugokite „snapshot“ iš Request-Headerių + Request-Body į debug spool failą. Tai nėra nuolatinis logavimas (duomenų apsauga/ apimtys), o kontroliuojamas įrankis sudėtingiems reprodukuojamiems atvejams iš produkcijos analizuoti.
Svarbu: snapshot niekada neturi be filtravimo persistentiškai saugoti Auth headerių, tokenų ar asmens duomenų. Praktikoje tai reiškia: Redaction (maskavimas) ir aktyvavimas tik per feature-flag arba whitelistą (pvz., tik tam tikroms Correlation-ID, ribotiems laiko langams).
Saubere Umsetzung in der Praxis: Maskieren statt Weglassen
Tikrose integracijose būtent „kritiniai“ laukai dažnai yra reikalingiausi debuggingui (pvz., identifikatoriai). Vietoje absoliutaus išmetimo geriau maskuoti: dalinai pakeisti tokenus, el. paštą palikti tik domeną, IBAN – tik paskutinius skaitmenis. Taip atvejis lieka reprodukuojamas, neplatindami perteklinių duomenų failų sistemoje. Papildomai snapshot turi būti aiškiai pažymėtas kaip debug artefaktas ir turėti apibrėžtą saugojimo trukmę.
Sauga ir eksploatavimas: antraščių perdavimas, proxy grandinės ir laiko limitai
Viena REST API retai baigiasi tiesiog prie kliento. Įprasta, kad egzistuoja grandinės, sudarytos iš reverse proxy, TLS terminavimo, WAF arba API-Gateway. Iš to kyla praktiniai punktai:
- Nuotolinis IP: Nepasitikėkite aklai
X-Forwarded-For. Priimkite jį tik iš patikimų proxy, kitu atveju naudokite tiesioginį soketo IP. Eksploatacijos vadove turi būti nurodyta, kurie hop’ai yra „trusted“. - Laiko limitai: Jei proxy turi 30 sekundžių, o jūsų backendui reikia 2 minučių, susidarys ghost-užklausos. Nustatykite laiko limitus nuosekliai visoje grandinėje ir apsispręskite: sinchroninis užklausimas ar job-pattern (202 Accepted + būsenos galinis taškas).
- Correlation-ID: Įtraukite Correlation-ID į atsakymų antraštes, kad administratoriai galėtų ją sujungti iš žurnalų ir kliento pusės. Jei gateway naudoja savo Request-IDs: loguokite ir atvaizduokite abi ID.
- Klaidos pranešimai: Produkcinėje aplinkoje neatskleiskite vidaus detalių. Debug-informacija tik kontroliuojamai (stage/feature-flag) ir, abejojant, tik žurnaluose.
Įvertinimas: kodėl RemObjects SDK čia gali turėti pranašumą
Delphi-ekosistemose REST-Server dažnai kuriami su lengvesniais framework’ais (pvz. minimalistiniais HTTP maršrutizatoriais). RemObjects SDK išryškina savo stipriąsias puses tada, kai jau turite arba jums reikalinga daugiasluoksnė architektūra:
- Aiškios paslaugų ribos: paslaugų metodai yra aiškiai apibrėžti, kontraktai yra versijuojami.
- Transportai ir serializacija: galite naudoti JSON, bet ir kitus žinučių formatus (priklausomai nuo konfigūracijos), neįmaišydami verslo logikos.
- Eksploatavimas: talpinimo parinktys ir integracija į esamus Windows- ir Linux-servisus yra planuojama, įskaitant nuoseklius diegimus.
Pateiktas požiūris papildo tai dalimis, kurių kasdienybėje dažnai trūksta: vienodi klaidų objektai, deterministinė versijavimo tvarka ir koreliuojamas logavimas. Ypač individualiai įmonių programinei įrangai su ilgu gyvavimo ciklu tai sutaupo laiko atnaujinimų ir išorinių sistemų integracijos metu.
Išvada: ar verta pastangų — ir kur šis požiūris tampa neefektyvus?
Pridėtinė vertė atsiranda tada, kai jūsų REST-sąsaja ne tik „veikia“, bet ir yra ilgalaikiškai eksploatuojama: stabilūs JSON-susitarimai, versijavimas be URL-chaoso, aiškios klaidos ir derinimas be spėliojimų. Būtent čia požiūris su Context, Correlation-ID ir centralizuotu Exception-Mapping RemObjects SDK yra stiprus.
Taikymo ribos: Jei turite tik vieną trumpalaikį galinį tašką be integracijos partnerių, Media-Type-versijavimas greitai atrodo kaip overengineering. Snapshot-žurnalavimas taip pat prasmingas tik tuomet, kai disciplinuotai įgyvendinate duomenų slėpimą (Redaction) ir jo įjungimą. Ir: jei jūsų proxy-stuokos antraštės „optimizuoja“ arba pašalina, pirmiausia turite sutvarkyti infrastruktūrą — kitaip derinsite neteisingą sluoksnį.
Jei modernizuojate esamą Delphi-serverių aplinką arba turite švariai integruoti procesui artimą programinį sprendimą į ERP/DMS/CRM, būtent šie mechanizmai dažnai lemia skirtumą tarp „veikia teste“ ir „veikia eksploatacijoje“.
Domeniniame kontekste taip pat svarbų vaidmenį atlieka Delphi REST-API ir REST-Serveris ir Remobjects Sdk Delphi, kai integracijos, duomenų srautai ir tolimesnė plėtra turi sklandžiai sąveikauti.
Kitas žingsnis
Kai tema virsta realiu projektu, architektūra, esami sprendimai ir eksploatavimas turėtų būti nagrinėjami kartu nuo pat pradžių.
Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.
- Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
- REST, duomenų prieiga, portalai ir rollout nebus perkelti į vėlesnį etapą kaip vėlyvos pasekmės.
- Jūs anksti matote, kuris kelias yra ekonomiškai ir operaciniškai tvarus.