Skip to content

Error Handling ​

When a resource method raises, MARS turns the exception into an HTTP response. You control the status code, content type and body either by raising the right exception type or by handling errors centrally with an invoke-error hook. The exception types live in MARS.Core.Exceptions.pas.

Exception hierarchy ​

Exception
└─ EMARSException
   └─ EMARSHttpException                 (status + content type + reason)
      ├─ EMARSEngineException
      └─ EMARSApplicationException
         ├─ EMARSResourceNotFoundException
         ├─ EMARSMethodNotFoundException
         ├─ EMARSAuthenticationException
         ├─ EMARSAuthorizationException
         └─ EMARSWithResponseException    (status + structured body)

Any exception that is not a MARS HTTP exception becomes 500 Internal Server Error, with a generic Internal server error body (the exception class and message are appended only in DEBUG builds, so production servers do not leak internals).

EMARSHttpException — status + message ​

The workhorse. Raise it to return a specific HTTP status with a plain-text (or custom content-type) message.

pascal
constructor Create(const AMessage: string;
  const AStatus: Integer = 500;
  const AContentType: string = TMediaType.TEXT_PLAIN_UTF8;
  const AReasonString: string = '');
pascal
[GET, Path('{id}')]
function GetById([PathParam] id: Integer): TItem;
begin
  if not TryLoad(id, Result) then
    raise EMARSHttpException.Create('Item not found', 404);
end;

Use CreateFmt for formatted messages:

pascal
raise EMARSHttpException.CreateFmt('Item %d not found', [id], 404);

The exception's Status, ContentType and ReasonString map directly onto the response.

Charset

The default content type is text/plain; charset=utf-8 (TMediaType.TEXT_PLAIN_UTF8), so a message containing non-ASCII characters (a localized exception text, a file name, …) reaches the client intact. The same applies to the generic 500 body produced for non-MARS exceptions.

EMARSWithResponseException — structured error body ​

When clients need machine-readable error details, raise EMARSWithResponseException with a payload. By default the payload is serialized using the normal MessageBodyWriter machinery (AUseMBW = True), so a record or object becomes JSON.

pascal
constructor Create(const AMessage: string;
  const AContent: TValue;
  const AStatus: Integer = 500;
  const AReasonString: string = '';
  const AContentType: string = TMediaType.APPLICATION_JSON;
  const AUseMBW: Boolean = True; const AIsReference: Boolean = False);

Example (from the ErrorObjects demo):

pascal
type
  TErrorDetails = record
    TimeStamp: TDateTime;
    Details: string;
    ReferenceNumber: Integer;
  end;

[GET, Path('MARSWithResponse')]
function RaiseDetailed: string;
var
  LError: TErrorDetails;
begin
  LError.TimeStamp := Now;
  LError.Details := 'Details about the error!';
  LError.ReferenceNumber := 123456;

  raise EMARSWithResponseException.Create(
    'Error Message!',
    TValue.From<TErrorDetails>(LError),
    530,                       // custom status
    'The reason of the error'  // reason phrase
  );
end;

The client receives status 530 and a JSON body describing the error.

Built-in exceptions raised by MARS ​

ExceptionStatusRaised when
EMARSResourceNotFoundException404The URL matches no resource.
EMARSMethodNotFoundException404No method matches the verb/path.
EMARSAuthenticationException403Authentication required but the token is missing/invalid/expired.
EMARSAuthorizationException403The token lacks an allowed role.
EMARSApplicationException400A parameter could not be bound because the request body is missing or malformed (see below).
EMARSApplicationException500Any other failure while binding a parameter value.

You can catch and re-map these in an invoke-error hook if you want different status codes or bodies.

Errors while binding parameters ​

Parameter binding happens during the setup phase, before your method body runs (see Request Lifecycle). When a binder or a MessageBodyReader raises, MARS wraps the failure in an EMARSApplicationException whose message names the resource, the method and the path:

Bad parameter value for method TItemResource.Create (/items).
Malformed or missing request body (JSON object expected)

If the original exception is an EMARSHttpException, its status is preserved through the wrapping; anything else falls back to 500. This is what lets a reader classify bad input as a client error:

pascal
// inside a custom IMessageBodyReader
if not Assigned(LJSON) then
  raise EMARSHttpException.Create('Malformed or missing request body', 400);

The built-in JSON readers already do this. A body that the client got wrong — absent, not parsable, or not shaped like the parameter it has to fill — produces a clean 400 Bad Request, instead of the 500 (or the silently empty value, later surfacing as an access violation on first field access) of earlier versions:

POST /items body[BodyParam] AItem: TItem (record or class)[BodyParam] AItems: TArray<TItem>
{"id":1,…}boundbound (array of one element)
[{"id":1,…},{"id":2,…}]400 — JSON object expectedbound
[1,2,3]400 — JSON object expected400 — JSON object expected at index 0
42, "text"400 — JSON object expected400 — JSON array expected
(empty body), not json at all400 — malformed or missing bodyempty array

Follow the same convention in your own readers: raise EMARSHttpException with 400 for input the client got wrong, and let genuinely unexpected failures surface as 500.

Centralized error handling ​

Register a global handler during ignition to map domain exceptions to HTTP responses in one place:

pascal
TMARSActivation.RegisterInvokeError(
  procedure (const AActivation: IMARSActivation; const AException: Exception;
    var AHandled: Boolean)
  begin
    if AException is EValidationError then
    begin
      AActivation.Response.StatusCode := 422;
      AActivation.Response.ContentType := TMediaType.APPLICATION_JSON;
      AActivation.Response.Content := ValidationErrorToJson(AException);
      AHandled := True;   // stop further default handling
    end;
  end);

Set AHandled := True to take ownership of the response; leave it False to fall through to MARS's default mapping.

You can also handle errors per resource with an [InvokeError] method (see Request Lifecycle), which is handy when only one resource needs special treatment.

Consuming errors on the client ​

The MARS client surfaces non-success responses through its OnError event and per-call exception callbacks, and EMARSWithResponseException bodies can be deserialized back into a record/object on the client side — see the ErrorObjects demo for the round trip.

Released under the Apache License 2.0.