From magazine topic to project implementation
Relevant service and technical pages for this post
Why „REST API with RemObjects SDK“ often decides at the edges in practice
A REST API with RemObjects SDK rarely stands or falls on the „Hello World“ service, but at the points where operation, legacy and integration collide: versioning without downtime, consistent error behavior across all endpoints, reproducible debugging in proxy chains and the ability to unambiguously correlate requests when problems occur.
RemObjects SDK provides a lot of infrastructure for this: services, message formats, serialization, hosting (e.g. as Windows- and Linux-services or behind IIS/Reverse Proxy) and defined places to handle errors centrally. What is often missing in mature business-software landscapes, however, is a consistently applied contract: Which JSON fields are stable? How do we signal errors? How do we reliably identify a request after it has passed through load balancers, TLS termination and multiple backend layers?
The following approach (including Delphi snippets) demonstrates a robust line for RemObjects SDK: version JSON contracts, enforce a Correlation-ID (request ID for tracing), translate exceptions into HTTP statuses and JSON error objects, and avoid pitting debugging and operations against each other. Additionally we look at edge cases that regularly occur in real environments: threading on the server, database accesses with BDE replacement with native binding, proxy headers, timeouts and „dirty“ client payloads.
Architecture decision: versioning via Media Type instead of URL
Many APIs version via paths like /v1/. That is pragmatic, but in long-running integrations (e.g. ERP/DMS/CRM integrations) it often leads to URL duplication, duplicated routes, duplicated tests and the operational question „Which version are we actually using?“ in runbooks.
An alternative is versioning via the Media Type (content negotiation). The client sends e.g. Accept: application/vnd.company.order+json;v=2. The server deterministically reads the version and adapts contract/DTO behavior. This works in proxy and cache chains if the headers are forwarded correctly. For administrators it is also easy to verify: a request can be reproduced with curl/Postman without differing URLs.
RemObjects SDK is not „REST-purist“, but a pragmatic service framework. Precisely for that reason the media-type variant is worth considering: you can keep stable endpoints while still evolving contracts. The important point is that you always evaluate the version, decide centrally in one place and propagate the result into your service context.
When does the Accept-header variant break?
In practice there are three common failure points you should address in advance:
- Proxy policies: Some reverse proxies/WAF rules normalize or filter Accept headers. Your API will then silently fall back to the default. Solution: explicitly inspect proxy rules, or fall back to
X-Api-Versionif necessary. - Client libraries: Some HTTP clients set their own Accept header and overwrite values. Solution: support the contract version also as an optional query parameter (only as a fallback), or parse the Accept header tolerant on the server side.
Accept (Vary: Accept), otherwise it will serve version 1 to version-2 clients. Solution: explicitly set Vary, or disable caching at the API level.Source snippet: Request context, Correlation-ID, version and error mapping
The code is intentionally structured to integrate into existing RemObjects server projects: a small context layer, a parser for the API version (from Accept), a correlation-ID mechanism and a central exception mapping. Terms:
- Correlation-ID: A unique ID per request that is returned in the response and referenced in logs.
- Exception-Mapping: Translation of internal Delphi exceptions into stable, client-processable error objects (including HTTP status).
- Contract-Version: Version of the JSON contract that controls behavior and fields.
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;
// Expects e.g.: 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;
// In production no internal details, no SQL, no paths.
// For debug/stage this can be extended via configuration.
begin
if E is EApiError then
Exit(E.Message);
if E is EArgumentException then
Exit('Invalid parameters.');
Exit('Internal error.');
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.Purpose: stable request context instead of “somewhere in the threadlocal”
The snippet deliberately separates concerns: TApiContext is the minimal state you want to pass through. In RemObjects SDK a lot is handled via server/channel context. In heterogeneous projects (e.g. additional worker threads, DB queue, background jobs) explicit propagation is often more robust than implicit threadlocals, because it makes concurrency and context switches more visible.
Prerequisites: The Accept-header variant assumes that your reverse proxy (nginx, IIS ARR, Traefik) forwards the header unchanged. In some environments “unusual” Accept headers are filtered or consolidated.
Pitfalls: Versioning via Accept is only as good as your tests. If clients use libraries that overwrite Accept, an API can suddenly fall back to the default. For legacy clients a default fallback makes sense, but it must be visible in monitoring (e.g. a log warning „Version defaulted“).
Variants: If you prefer to version via X-Api-Version: the parser is identical, only the source is a different header. From the gateway perspective that can be easier to control.
Integration into RemObjects SDK: Correlation-ID and Exception Mapping at the service entry
The real effect arises when you apply the mechanism consistently at the edge of your server: once at request entry to read from headers, once at exception exit to translate into a stable response. Depending on hosting (e.g. RO-HTTP-Server, IIS-Hosting, self-hosted Windows-/Windows- and Linux-services) the concrete hook points differ; the principle remains the same: build context, invoke business logic, map exceptions centrally.
In RemObjects projects it is common to work directly per service method. That scales well at first, but breaks down in operation: every method builds logging and error handling differently. A clean separation is a service base or a dispatcher that standardizes those concerns.
Practical procedure (deliberately short and implementation-oriented)
- Read Correlation-ID from request header
X-Correlation-ID; if missing, generate server-side (e.g. GUID). - Read contract version from
Accept(or fromX-Api-Version). - Log request start: method, path, Correlation-ID, remote IP, start timing.
- Execute business logic; encapsulate DB accesses transactionally where possible.
- Catch exceptions: determine HTTP status, create JSON error object, set response header
X-Correlation-ID. - Log request end: status, duration, if applicable error code.
Threading on the server: why Correlation-ID becomes worthless without context discipline
A common Delphi edge case: the service method triggers asynchronous work (e.g. report generation, import, push into a DMS). Then the original request thread is no longer the one that later writes log lines. If the Correlation-ID is only known “at the beginning”, traceability breaks down.
Pragmatic rule: anything that does not remain strictly in the request thread should be passed the context explicitly. Even if that results in longer parameter lists, it pays off. Alternatively, work with a clearly defined context object that is intentionally passed to workers (instead of global variables or hidden singletons).
Typical tipping points in RemObjects-/Delphi-servers:
- DB connections per thread: BDE-Ablosung mit nativer Anbindung-connections are not automatically safely shareable between threads. A connection pool or one connection per thread is often more sensible than „a global connection“.
- Transaction boundaries: If you have multiple steps within a request that belong together, the transaction must remain within the same logical unit. Asynchronous work must not „accidentally“ continue within the same transaction.
- Cancellation: When the client aborts (proxy timeout, browser closed), the server often continues running. Deliberately consider whether background work still makes sense in that case.
Data access and error codes: 409 is not „also a 500“
In integration projects, clean error mapping is more than cosmetic. It determines whether a counterpart (ERP-Connector, ETL job, customer portal) can react correctly. A few practical guardrails that have proven useful in Delphi/RemObjects environments:
- 400 Bad Request: Validation, missing/invalid parameters, JSON not parseable. Important: The response should remain stable even if the body is malformed.
- 401/403: Separate authentication from authorization. 401 means „no/invalid identity“, 403 „identity OK, but forbidden“.
- 404: Resource does not exist. Be careful with security: do not always reveal whether something exists.
- 409 Conflict: Domain-level conflict (e.g. version conflict, „status does not allow this action“, unique key violation when it is business-relevant).
- 422 Unprocessable Content: When syntax is fine but domain validation fails (not every team uses 422, but it is often clearer than 400).
- 500: Anything you cannot classify cleanly. This also includes „DB down“, „timeout“, „unhandled exception“.
Delphi-specific tip: Many DB errors surface as generic exceptions. It is worth detecting known situations in the data access layer and translating them into EApiError. Important: Do not include SQL fragments or internal table/column names in the client message. Those details belong in the log, not in the response.
Debugging tip: reproducible errors via a „Contract Snapshot“
Unusual, but extremely helpful in operation: when errors occur (or deliberately for certain Correlation-IDs), save a „snapshot“ of request headers + request body to a debug spool file. This is not permanent logging (data protection/volume), but a controlled tool to reproduce hard-to-recreate cases from near-production context.
Important: A snapshot must never persist unfiltered auth headers, tokens or personal data. In practice this means: redaction (masking) and activation only via a feature flag or whitelist (e.g. only for certain Correlation-IDs, short time windows).
Practical, tidy implementation: mask rather than omit
In real integrations the „critical“ fields are often exactly those you need for debugging (e.g. identifiers). Instead of blanket omission, masking is better: partially replace tokens, keep only the domain of an email address, store only the last digits of an IBAN. This keeps the case reproducible without distributing unnecessary data in the file system. Additionally, the snapshot should be clearly marked as a debug artifact and have a defined retention period.
Security and Operations: Header Forwarding, Proxy Chains and Timeouts
An REST API rarely terminates directly at the client. Typical deployments include chains of reverse proxies, TLS termination, WAFs or API gateways. This leads to practical considerations:
- Remote IP: Do not trust
X-Forwarded-Forblindly. Only accept it from trusted proxies and otherwise use the direct socket IP. Operational manuals should specify which hops are “trusted.” - Timeouts: If a proxy has a 30-second timeout but your backend needs 2 minutes, you will produce ghost requests. Set timeouts consistently along the chain and decide: synchronous request or job pattern (202 Accepted + status endpoint).
- Correlation-ID: Set the Correlation-ID in response headers so admins can correlate it between logs and the client side. If a gateway issues its own request IDs: log and map both IDs.
- Error texts: No internal details in production. Debug details only under controlled conditions (stage / feature flag) and, if in doubt, only in the logs.
Context: Why RemObjects SDK can be advantageous here
In Delphi ecosystems, REST-servers are often built with lighter frameworks (e.g. minimal HTTP routers). RemObjects SDK shows its strengths when you already have or need a multi-layer architecture:
- Clear service boundaries: Service methods are explicit; contracts are versionable.
- Transports and serialization: You can speak JSON but also other message formats (depending on setup) without mixing transport concerns into business logic.
- Operations: Hosting options and integration into existing Windows- and Linux-services are plannable, including clean rollouts.
The presented approach adds the pieces that are often missing in day-to-day work: uniform error objects, deterministic versioning and correlatable logging. Especially for custom enterprise software with long lifecycles, this saves time on updates and on integration with external systems.
Conclusion: Is the effort worth it — and where does this approach break down?
The benefit arises when your REST interface not only “works”, but is sustainably operable: stable JSON contracts, versioning without URL proliferation, traceable errors and debugging without guesswork. This is precisely where the approach with Context, Correlation-ID and central exception mapping in RemObjects SDK is strong.
Limitations: If you only have a single, short-lived endpoint without integration partners, media-type versioning quickly becomes overengineering. Snapshot logging also only makes sense if you implement redaction and activation in a disciplined way. And: if your proxy stack “optimizes” or removes headers, you must correct the infrastructure first, otherwise you will be debugging the wrong layer.
If you modernize an existing Delphi server landscape or need to integrate a process-near software solution cleanly into ERP/DMS/CRM, these mechanisms are often the difference between “works in testing” and “works in production.”
In the functional domain, Delphi REST-API and REST-Server and Remobjects Sdk Delphi also play an important role when integrations, data flows and ongoing development must interoperate cleanly.
Discuss a project or modernization initiative with Net-Base.
Next step
When the topic becomes a real project, architecture, the existing system landscape and operations should be considered together early on.
We support not only with individual issues, but also when source snippets, legacy topics, or portal ideas are to be turned into a robust enterprise project.
- Current state, target state and technical risks are assessed jointly.
- REST, data access, portals and rollout are not deferred as afterthoughts.
- You can determine early which path is economically and operationally viable.