horse-request-guard

September 15, 2026 · View on GitHub

Input validation middleware for the Horse web framework.

License: MIT Delphi FPC Horse Boss


What it does

Registers a single middleware that validates each incoming request before it reaches your route handlers. On the first failing check the request is rejected with a specific HTTP status code and the middleware chain stops.

#CheckFailure status
1–2HTTP method in AllowedMethods (TRACE and CONNECT are rejected because the default list leaves them out)405
3Host header present and printable (no control characters)400
4Host matches AllowedHosts (if configured)400
5Content-Length and Transfer-Encoding not both present (RFC 7230 §3.3.3)400
6Request path ≤ MaxUrlLength414
7Each query-string key ≤ MaxQueryKeyLen, value ≤ MaxQueryValueLen400
8Number of request headers ≤ MaxHeaderCount431
9Declared Content-LengthMaxBodyBytes413

Installation

Requires Horse 3.1.0 or later.

boss install github.com/freitasjca/horse-request-guard

Or add to your project's boss.json:

{
  "dependencies": {
    "github.com/freitasjca/horse-request-guard": ">=1.0.0"
  }
}

Usage

Default configuration

uses
  Horse,
  Horse.Middleware.RequestGuard;

begin
  THorse.Use(THorseRequestGuard.New);   // must be the first middleware

  THorse.Get('/ping',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin
      Res.Send('pong');
    end);

  THorse.Listen(9000);
end.

Custom configuration

uses
  Horse,
  Horse.Middleware.RequestGuard;

var
  LGuardConfig: THorseRequestGuardConfig;
begin
  LGuardConfig                := THorseRequestGuardConfig.Default;
  LGuardConfig.AllowedMethods := ['GET', 'POST'];          // restrict to two verbs
  LGuardConfig.AllowedHosts   := ['api.example.com'];      // lock to one hostname
  LGuardConfig.MaxBodyBytes   := 1 * 1024 * 1024;          // 1 MB body limit

  THorse.Use(THorseRequestGuard.New(LGuardConfig));

  THorse.Listen(9000);
end.

The configuration is process-wide: a second THorseRequestGuard.New call replaces the first, so register the guard once.


Configuration reference

type
  THorseRequestGuardConfig = record
    AllowedMethods:   TArray<string>;  // default: GET POST PUT DELETE PATCH HEAD OPTIONS
    AllowedHosts:     TArray<string>;  // default: [] = accept any host
    MaxUrlLength:     Integer;         // default: 8 192 characters  (0 = unlimited)
    MaxQueryKeyLen:   Integer;         // default: 2 048 characters  (0 = unlimited)
    MaxQueryValueLen: Integer;         // default: 2 048 characters  (0 = unlimited)
    MaxHeaderCount:   Integer;         // default: 100 headers       (0 = unlimited)
    MaxBodyBytes:     Int64;           // default: 4 194 304 bytes   (0 = unlimited)
    RejectCLWithTE:   Boolean;         // default: True
    class function Default: THorseRequestGuardConfig; static;
  end;
FieldDefaultDescription
AllowedMethodsGET POST PUT DELETE PATCH HEAD OPTIONSAccepted HTTP verbs, compared case-insensitively with the method as sent. TRACE and CONNECT are rejected because the default list leaves them out; adding them to the list allows them. An empty list disables the method check.
AllowedHosts(empty — any)If non-empty, the Host header must match one of these values (case-insensitive).
MaxUrlLength8192Maximum length of the decoded request path in characters.
MaxQueryKeyLen2048Maximum length of a single query-string key.
MaxQueryValueLen2048Maximum length of a single query-string value.
MaxHeaderCount100Maximum number of request headers.
MaxBodyBytes4194304 (4 MB)Maximum value accepted in the Content-Length header.
RejectCLWithTETrueReject requests that carry both Content-Length and Transfer-Encoding (request-smuggling guard, RFC 7230 §3.3.3).

Provider compatibility

FeatureHorse + IndyHorse + CrossSocket
Method allowlist (checks 1–2)this middleware✓ pre-pipeline ¹
Host validation (checks 3–4)this middleware✓ pre-pipeline ¹
CL+TE smuggling guard (check 5)this middleware✓ pre-pipeline ¹
Size limits (checks 6–9)this middleware✓ pre-pipeline ¹

¹ The Horse.Provider.CrossSocket provider enforces all of these checks before the request enters the Horse pipeline via TRequestBridge.Populate. Registering this middleware alongside CrossSocket is safe (defence in depth) but redundant — invalid requests never reach the middleware on the CrossSocket path.

On Indy, this middleware is the only line of defence. Register it as the first middleware so that invalid requests are rejected before any application logic runs.

FPC note

The method check reads the method string as sent (Req.RawWebRequest.Method), not TMethodType, so OPTIONS, TRACE and CONNECT are told apart on both Delphi and Free Pascal. On FPC without HORSE_FPC_FUNCTIONREFERENCES, Horse callbacks must be plain procedures; the guard is one, so no extra define is needed. The FPC build has not been verified yet.


License

MIT — see LICENSE.