Fra magasinets tema til projektpraksis
Passende service- og tekniske sider til artiklen
Hvorfor „REST API med RemObjects SDK“ i praksis ofte afgør ved grænserne
En REST API med RemObjects SDK afgøres sjældent ved „Hello World“-servicen, men ved de steder, hvor drift, Legacy og integration støder sammen: versionsstyring uden stop, konsistent fejladfærd på tværs af alle endepunkter, reproducerbar debugging i proxy-kæder og evnen til entydigt at korrelere forespørgsler i fejltilfælde.
RemObjects SDK leverer til det formål meget infrastruktur: services, meddelelsesformater, serialisering, hosting (f.eks. som Windows- og Linux-Services eller bag IIS/Reverse Proxy) og definerede steder til central fejlhåndtering. Hvad der i etablerede forretningssoftware-landskaber ofte mangler, er en konsekvent gennemført kontrakt: Hvilke JSON-felter er stabile? Hvordan signalerer vi fejl? Hvordan genkender vi en request, når den har passeret Load Balancer, TLS-termination og flere backend-lag?
Følgende tilgang (inklusive Delphi-snipsel) viser en robust linje for RemObjects SDK: versionér JSON-kontrakter, påkræv Correlation-ID (Request-ID til sporing), oversæt Exceptions til HTTP-status og JSON-fejlobjekter og undgå at spille debugging og drift ud imod hinanden. Derudover ser vi på kanttilfælde, som regelmæssigt optræder i reelle miljøer: threading på serveren, databaseadgang i forbindelse med BDE-Ablösung med nativer binding, proxy-headers, timeouts og „beskidte“ klient-payloads.
Arkitekturbeslutning: Versionering via medietype i stedet for URL
Mange API’er versionerer via stier som /v1/. Det er pragmatisk, men i længerevarende integrationer (f.eks. ERP/DMS/CRM-tilknytninger) fører det ofte til duplikation af URLs, dobbelte ruter, dobbelte tests og „Hvilken version bruger vi egentlig?“ i driftshåndbøger.
Et alternativ er versionering via den Media Type (Content Negotiation). Klienten sender f.eks. Accept: application/vnd.company.order+json;v=2. Serveren læser versionen deterministisk og tilpasser contract/DTO-adfærd. Det fungerer i proxy- og cache-kæder, hvis headerne videreføres korrekt. For administratorer er det desuden let at verificere: En request kan reproduceres med Curl/Postman uden at ændre URL’erne.
RemObjects SDK er ikke „REST-puristisch“, men et pragmatisk service-framework. Netop derfor giver medietype-tilgangen mening: I kan bevare stabile endepunkter og alligevel videreudvikle kontrakter. Det er vigtigt, at I altid evaluerer versionen centralt ét sted og fører resultatet ind i jeres service-kontekst.
Hvornår svigter Accept-header-varianten?
I praksis er der tre typiske brudflader, som man bør adressere på forhånd:
- Proxy-politikker: Nogle Reverse Proxies/WAF-regler normaliserer eller filtrerer Accept-header. Så falder jeres API stille tilbage til standard. Løsning: Gennemgå proxy-regler eksplicit, evt. falde tilbage til
X-Api-Version. - Client-Libraries: Nogle HTTP-clients sætter egne Accept-headers og overskriver værdier. Løsning: Understøt kontrakt-version også som en valgfri query-parameter (kun som fallback), eller parse Accept-headeren tolerant på serversiden.
Accept (Vary: Accept), ellers leverer den version 1 til version-2-klienter. Løsning: sæt Vary bevidst, eller deaktiver caching på API-niveau.Kildeudsnit: Request-Context, Correlation-ID, Version og Error-Mapping
Koden er bevidst skåret, så den kan integreres i eksisterende RemObjects-serverprojekter: et lille Context-lag, en parser til API-versionen (fra Accept), en Correlation-ID-mekanisme og et centralt Exception-Mapping. Begreber:
- Correlation-ID: Entydig ID pr. request, som indgår i responsen og kan refereres i logs.
- Exception-Mapping: Oversættelse af interne Delphi-Exceptions til stabile fejlobjekter, der kan håndteres af klienten (inkl. HTTP-status).
- Contract-Version: Version af JSON-kontrakten, som styrer adfærd og felter.
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.Formål: Stabil request-kontekst i stedet for „et eller andet i Threadlocal“
Uddraget adskiller bevidst: TApiContext er den minimale tilstand, du vil videregive. I RemObjects SDK går meget via server-/channel-kontekst. I heterogene projekter (fx ekstra worker-tråde, DB-queue, baggrundsjob) er eksplicit videregivelse ofte mere robust end implicitte Threadlocals, fordi det gør samtidighed og kontekstskift mere synlige.
Randbetingelser: Accept-header-varianten forudsætter, at din reverse proxy (nginx, IIS ARR, Traefik) videresender headeren uændret. I nogle miljøer filtreres eller sammenfletter man „uvanlige“ Accept-headere.
Faldgruber: Versionering via Accept er kun så god som dine tests. Hvis klienter bruger biblioteker, der overskriver Accept, kan en API pludselig falde tilbage til default. For legacy-klienter er et default-fallback fornuftigt, men det skal være synligt i monitorering (fx log-advarsel „Version defaulted“).
Varianter: Hvis du foretrækker at versionere via X-Api-Version: parseren er identisk, kun kilden er en anden header. Set fra gatewayers perspektiv er det nogle gange nemmere at kontrollere.
Integration i RemObjects SDK: Correlation-ID og Exception-Mapping ved serviceindgang
Den egentlige effekt opnås, når du anvender mekanikken konsekvent i kanten af din server: læs fra headerne én gang ved request-indgang, og oversæt én gang ved undtagelsesudgang til et stabilt Response. Afhængigt af hosting (fx RO-HTTP-Server, IIS-hosting, selvbetjent Windows-/Windows- og Linux-Services) varierer de konkrete hook-punkter; princippet er det samme: byg context, kald forretningslogik, map undtagelser centralt.
I RemObjects-projekter arbejder man ofte direkte pr. service-metode. Det skalerer godt i starten, men svigter i drift: Hver metode implementerer logging og fejlhåndtering forskelligt. En ren afgrænsning er en fælles servicebase eller en dispatcher, der standardiserer.
Praktisk forløb (bevidst kort og implementeringsnært)
- Læs Correlation-ID fra request-header
X-Correlation-ID; hvis den mangler, generer den server-side (fx GUID). - Læs kontrakt-version fra
Accept(eller fraX-Api-Version). - Log request-start: metode, sti, Correlation-ID, remote IP; start måling af varighed.
- Udfør forretningslogik; kapsl DB-adgang så vidt muligt transaktionelt.
- Opfang undtagelser: bestem HTTP-status, opret JSON-fejlobjekt, sæt Response-header
X-Correlation-ID. - Log request-slut: status, varighed, evt. fejlkode.
Trådning på serveren: Hvorfor Correlation-ID uden kontekstdisciplin bliver værdiløs
Et hyppigt Delphi-kanttilfælde: Service-metoden triggere asynkront arbejde (fx rapportgenerering, import, push til et DMS). Så er den oprindelige request-tråd ikke længere den, der senere skriver loglinjer. Hvis Correlation-ID kun er kendt „i starten“, bryder sporbarheden sammen.
Pragmatisk regel: Alt, der ikke forbliver strengt i request-tråden, får konteksten eksplicit overleveret. Selv om det betyder flere parameterrækker, betaler det sig. Alternativt kan man arbejde med et klart defineret kontekst-objekt, som bevidst gives til workeren (i stedet for globale variabler eller skjulte singletons).
Typiske brudpunkter i RemObjects-/Delphi-servere:
- DB-forbindelser per tråd: BDE-Ablosung mit nativer Anbindung-Verbindungen deles ikke automatisk trådsikkert. En Connection-Pool eller én forbindelse per tråd er ofte mere rimelig end „en global Connection“.
- Transaktionsgrænser: Hvis I inden for et request har flere trin, der hører sammen, skal transactionen forblive i samme logiske enhed. Asynkront arbejde må ikke „ved et uheld“ fortsætte i den samme transaction.
- Aflysning: Hvis klienten afbryder (proxy timeout, browser lukket), kører serveren ofte videre. Overvej bevidst, om baggrundsarbejde så stadig giver mening.
Dataadgang og fejlkoder: 409 er ikke „også en 500“
I integrationsprojekter er konsekvent error-mapping mere end kosmetik. Det afgør, om en modpart (ERP-Connector, ETL-Job, Kundeportal) kan reagere korrekt. Et par praksisnære pejlemærker, som har vist sig nyttige i Delphi/RemObjects-miljøer:
- 400 Bad Request: Validering, manglende/ugyldige parametre, JSON ikke parsebar. Vigtigt: Svaret skal forblive stabilt, også hvis body er ødelagt.
- 401/403: Skelnen mellem autentifikation og autorisation. 401 betyder „ingen/ugyldig identitet“, 403 „identitet ok, men forbudt“.
- 404: Ressource findes ikke. Vær forsigtig med sikkerhed: Afslør ikke altid, om noget eksisterer.
- 409 Conflict: Faglig konflikt (f.eks. versionskonflikt, „status tillader ikke denne handling“, entydig nøgleovertrædelse, hvis den er fagligt relevant).
- 422 Unprocessable Content: Når syntaksen er ok, men faglig validering fejler (ikke hvert team bruger 422, men det er ofte tydeligere end 400).
- 500: Alt, hvad I ikke kan klassificere klart. Det inkluderer også „DB down“, „Timeout“, „Unhandled Exception“.
Delphi-specifikt trick: Mange DB-fejl kommer op som generiske Exceptions. Det kan betale sig i dataadgangslaget målrettet at tjekke for kendte situationer og overføre dem til EApiError. Vigtigt i den sammenhæng: Inddrag ikke SQL-fragmenter eller interne tabel-/kolonnenavne i client-beskeden. Disse detaljer hører i loggen, ikke i response.
Debugging-trick: reproducerbare fejl gennem „Contract Snapshot“
Usædvanligt, men i drift ekstremt nyttigt: Gem ved fejl (eller målrettet for bestemte Correlation-IDs) et „snapshot“ af Request-headere + Request-body i en debug-spool-fil. Det er ikke permanent logging (databeskyttelse/volumen), men et kontrolleret værktøj til at genskabe svært reproducerbare tilfælde tæt på produktion.
Vigtigt: Et snapshot må aldrig ufiltreret persistentgøre auth-headere, tokens eller personoplysninger. I praksis betyder det: Redaction (maskering) og aktivering kun via feature-flag eller whitelist (f.eks. kun for bestemte Correlation-IDs, korte tidsvinduer).
Ren implementering i praksis: Maskering fremfor udeladelse
I reelle integrationer er det ofte de „kritiske“ felter, man har brug for til debugging (f.eks. identifikatorer). I stedet for blank udeladelse er maskering bedre: Erstat dele af tokens, behold kun domænet i e-mailadresser, IBAN kun de sidste cifre. Så forbliver sagen reproducerbar uden at sprede unødvendige data i filsystemet. Derudover bør snapshot’et være klart markeret som et debug-artefakt og have en defineret opbevaringstid.
Sikkerhed og drift: videresendelse af headers, proxy-kæder og timeouts
En REST API ender sjældent direkte ved klienten. Typisk ses kæder af reverse proxy, TLS-terminering, WAF eller API-gateway. Heraf følger praktiske punkter:
- Remote IP: Stol ikke blindt på
X-Forwarded-For. Accepter kun værdier fra betroede proxies, ellers brug den direkte socket-IP. I driftsmanualer bør det fremgå, hvilke hops der er „trusted“. - Timeouts: Hvis en proxy har 30 sekunder, men dit backend kræver 2 minutter, skaber du Ghost-Requests. Sæt timeouts konsistent langs kæden og beslut: synkrone requests eller job-mønster (202 Accepted + status-endpoint).
- Correlation-ID: Sæt Correlation-ID i response-headere, så administratorer kan korrelere dem mellem logs og klientside. Hvis et gateway bruger egne request-IDs: log og kortlæg begge IDs.
- Fejltekster: I produktionsdrift ingen interne detaljer. Debug-oplysninger kun kontrolleret (stage/feature-flag) og i tvivlstilfælde kun i loggen.
Vurdering: Hvorfor RemObjects SDK kan være en fordel her
I Delphi-økosystemer bygges REST-server ofte med lettere frameworks (fx minimalistiske HTTP-routere). RemObjects SDK udspiller sine styrker, når I allerede har eller har brug for en flerlagsarkitektur:
- Klare servicegrænser: Servicemetoder er eksplicitte, kontrakter kan versionsstyres.
- Transporte og serialisering: I kan tale JSON, men også andre meddelelsesformater (afhængig af setup), uden at forretningslogik blandes sammen.
- Drift: Hostingmuligheder og integration i eksisterende Windows- og Linux-services er planlæggelige, inklusive kontrollerede udrulninger.
Den viste tilgang supplerer dette med de dele, der ofte mangler i hverdagen: ensartede fejlobjekter, deterministisk versionering og korrelerbart logging. Især for individuel virksomhedsoftware med lange livscyklusser sparer I tid ved opdateringer og ved integration af eksterne systemer.
Konklusion: Betaler indsatsen sig – og hvor bryder tilgangen sammen?
Værdien opstår, når jeres REST-interface ikke bare «virker», men er holdbar i drift: stabile JSON-kontrakter, versionering uden URL-rod, forståelige fejl og debugging uden gætterier. Netop her står tilgangen med Context, Correlation-ID og centralt exception-mapping i RemObjects SDK stærkt.
Begrænsninger: Hvis I kun har et enkelt, kortlivet endpoint uden integrationspartnere, vil media-type-versionering hurtigt virke som overengineering. Snapshot-logging giver kun mening, hvis I disciplineret implementerer redaction og aktivering. Og: hvis jeres proxy-stack „optimerer“ eller fjerner headers, skal infrastrukturen rettes først, ellers debugger I på den forkerte lag.
Hvis I moderniserer en eksisterende Delphi-serverlandskab eller skal integrere en procesnær softwareløsning rent i ERP/DMS/CRM, er netop disse mekanismer ofte forskellen mellem „kører i test“ og „kører i drift“.
I det faglige domæne spiller også Delphi REST-API og REST-server og Remobjects Sdk Delphi en vigtig rolle, når integrationer, datatrømme og videreudvikling skal fungere gnidningsløst.
Næste trin
Når emnet bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.
Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.
- Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
- REST, dataadgang, portaler og udrulning bliver ikke udskudt som efterfølgende opgaver.
- De ser tidligt, hvilken vej der er økonomisk og driftsmæssigt bæredygtig.