Request & Response

June 22, 2026 · View on GitHub

Read this in English or Português (BR).

Every Horse route callback receives a THorseRequest and a THorseResponse. This page is the API reference for both.

procedure(Req: THorseRequest; Res: THorseResponse)

For the route declaration itself, see Routing. For middleware that wraps these objects, see Middleware.


THorseRequest

AccessorTypeWhat it returns
BodystringRaw request body decoded as UTF-8. Idempotent — multiple reads return the same cached string.
Body<T>genericReturns FBody as T — used when middleware (e.g. Jhonson) parses the body into an object.
Body(AObject) / Body(AObject, AOwnsBody)setterUsed by middleware to attach a parsed body object. With AOwnsBody = True (the default / 1-arg form) Horse owns and frees the object, freeing any previous owned value first. Transports whose body is a non-owning reference into a socket buffer (e.g. CrossSocket) pass AOwnsBody = False so Clear nils the reference without freeing it.
ParamsTHorseCoreParamRoute path parameters: Req.Params['id'].
QueryTHorseCoreParamURL query string: Req.Query['name'].
HeadersTHorseCoreParamRequest headers: Req.Headers['Content-Type']. Case-insensitive lookup.
CookieTHorseCoreParamParsed Cookie: header: Req.Cookie['session'].
ContentFieldsTHorseCoreParamParsed application/x-www-form-urlencoded body fields.
SessionsTHorseSessionsServer-side session map (one per request).
MethodstringRaw HTTP verb: 'GET', 'POST', 'OPTIONS', etc.
MethodTypeTMethodTypeEnum form (see Routing).
PathInfostringDecoded path: /users/42.
HoststringHost: header value.
ContentTypestringContent-Type request header.
RawWebRequestTWebRequest / TRequestThe underlying provider object — Indy's TIdHTTPRequestInfo for the default provider, an adapter for non-Indy providers.

Reading a request body

THorse.Post('/echo',
  procedure(Req: THorseRequest; Res: THorseResponse)
  begin
    // Raw text body
    Res.Send('You sent: ' + Req.Body);
  end);

For JSON, register the Jhonson middleware once at startup and then use Body<TJSONObject>:

uses Horse, Horse.Jhonson, System.JSON;

THorse.Use(Jhonson);

THorse.Post('/items',
  procedure(Req: THorseRequest; Res: THorseResponse)
  var
    Json: TJSONObject;
  begin
    Json := Req.Body<TJSONObject>;
    Res.Send('Got: ' + Json.GetValue('name').Value);
  end);

Reading params, query, headers

All four return THorseCoreParam, a dictionary-like accessor:

THorse.Get('/search/:type',
  procedure(Req: THorseRequest; Res: THorseResponse)
  var
    Limit: Integer;
  begin
    if not TryStrToInt(Req.Query['limit'], Limit) then
      Limit := 10;

    Res.Send(Format('Searching %s, limit %d, by %s',
      [Req.Params['type'], Limit, Req.Headers['X-User']]));
  end);

THorseCoreParam:

  • Items[name: string]: string — default indexer, returns '' if absent.
  • TryGetValue(name; out value): Boolean — distinguish absent vs empty.
  • Dictionary: TDictionary<string,string> (Delphi) / TStringList (FPC) — direct collection access if you need to iterate.

File uploads

multipart/form-data requests populate Req.ContentFields:

THorse.Post('/upload',
  procedure(Req: THorseRequest; Res: THorseResponse)
  var
    Stream: TStream;
  begin
    Stream := Req.ContentFields.Field('file').AsStream;   // file field (text fields: Field('x').AsString)
    try
      Stream.SaveToFile('uploaded.bin');
      Res.Send('Saved ' + IntToStr(Stream.Size) + ' bytes');
    finally
      // CrossSocket path: do NOT free — it's a non-owning ref.
      // Indy path: also do not free; THorseRequest manages it.
    end;
  end);

THorseResponse

MethodReturnsEffect
Send(AContent: string)THorseResponse (fluent)Writes a string body. Default status 200.
Send<T>(AContent: T)THorseResponseWrites an object; the middleware chain typically serialises it (e.g. JSON via Jhonson).
Status(AStatus: Integer)THorseResponseSets HTTP status code. Default 200.
Status(AStatus: THTTPStatus)THorseResponseTyped variant — e.g. THTTPStatus.NotFound.
Status (no arg)IntegerReads the currently-set status.
ContentType(AContentType: string)THorseResponseSets Content-Type header.
AddHeader(AName, AValue: string)THorseResponseAdds a response header.
RemoveHeader(AName: string)THorseResponseRemoves a previously-added header.
RedirectTo(ALocation: string)THorseResponseSends 302 Found with Location:.
RedirectTo(ALocation, AStatus)THorseResponseLets you choose 301, 307, etc.
SendFile(AFileName: string)THorseResponseStreams a file as the body; sets Content-Type from the extension.
SendFile(AStream, AFileName, AContentType)THorseResponseStreams an in-memory stream.
Download(AFileName: string)THorseResponseLike SendFile but adds Content-Disposition: attachment.
Download(AStream, AFileName, AContentType)THorseResponseStream + attachment header.
Render(AFileName: string)THorseResponseStreams a file inline (no attachment header).
RawWebResponseTWebResponse / TResponseUnderlying provider response. Used by middleware that needs direct access.

Examples

Plain text:

Res.ContentType('text/plain').Send('hello');

JSON (via Jhonson):

uses System.JSON;

var Json := TJSONObject.Create;
Json.AddPair('ok', TJSONBool.Create(True));
Res.Send<TJSONObject>(Json);   // Jhonson serialises + frees

Status + body for an error:

Res.Status(THTTPStatus.BadRequest)
   .ContentType('application/json')
   .Send('{"error":"missing field"}');

Redirect:

Res.RedirectTo('/login');

File download:

Res.Download('reports/2026-05.pdf');
// Sends Content-Disposition: attachment; filename="2026-05.pdf"

Stream a generated file:

var Stream := TMemoryStream.Create;
GenerateCSV(Stream);
Stream.Position := 0;
Res.Download(Stream, 'export.csv', 'text/csv');

Adding a custom header:

Res.AddHeader('X-Rate-Limit-Remaining', '47').Send('ok');

Status helpers

Horse.Commons.THTTPStatus provides named constants for the common codes — preferred over magic numbers:

Res.Status(THTTPStatus.OK);              // 200
Res.Status(THTTPStatus.Created);         // 201
Res.Status(THTTPStatus.NoContent);       // 204
Res.Status(THTTPStatus.BadRequest);      // 400
Res.Status(THTTPStatus.Unauthorized);    // 401
Res.Status(THTTPStatus.NotFound);        // 404
Res.Status(THTTPStatus.InternalServerError); // 500

Horse.Commons.THTTPStatusHelper.ToString converts the enum back to its standard reason phrase if you ever need to print it.

Errors and exceptions

Raising EHorseException from a callback short-circuits the response:

uses Horse.Exception;

THorse.Get('/secret',
  procedure(Req: THorseRequest; Res: THorseResponse)
  begin
    if Req.Headers['X-Auth'] <> 'secret' then
      raise EHorseException.New.Status(THTTPStatus.Unauthorized).Error('Bad token');
    Res.Send('welcome');
  end);

The framework converts the exception to a JSON error response. Any other uncaught exception becomes a 500 with a generic body.

To intercept exceptions globally, register the handle-exception middleware (HashLoad/handle-exception) — it formats your errors consistently.


Provider-specific notes

Most application code never needs to think about the transport. A few exceptions:

  • Body ownership on CrossSocket: Req.Body<TStream> on the CrossSocket provider returns a non-owning reference into the receive buffer. Never Free it. If you need the stream after the request returns, copy into a TMemoryStream you own. (Doesn't apply to Indy — Indy gives you its own owned stream.)
  • Body ownership on mORMot2: the mORMot provider does not produce a TStream body at all — the request body is buffered as a RawByteString (InContent) owned by mORMot. Req.Body: string is decoded once at request entry and cached (PATCH-REQ-9), so reading it multiple times is O(1). Req.Body<TStream> is therefore not the right pattern on this transport; use Req.Body: string (text) or Req.RawWebRequest.Content (raw bytes) instead.
  • Concurrent handlers: Indy runs one thread per connection; CrossSocket dispatches to an IO thread pool (and an optional Horse worker pool); mORMot2 dispatches to its own fixed thread pool inside THttpServer (default 32, configurable). In every case your handler runs to completion on a single thread, so per-request state is safe. Shared state needs explicit locking (TCriticalSection, TMonitor).
  • Req.RawWebRequest and Res.RawWebResponse: middleware that pokes the underlying objects (e.g. Horse.CORS setting Access-Control-Allow-Origin directly) keeps working across every Provider — CrossSocket and mORMot2 both return an adapter object backed by the same IHorseRawRequest / IHorseRawResponse interface that exposes the same surface as the Indy TIdHTTPAppRequest / TIdHTTPAppResponse.

See Providers for the full breakdown.

See also

  • Routing — declare the routes that produce these callbacks.
  • Middleware — wrap callbacks with cross-cutting logic.
  • Middleware EcosystemJhonson (JSON), CORS, JWT, compression, and more.