horse-request-guard

April 12, 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
1HTTP method in AllowedMethods405
2TRACE / CONNECT blocked unconditionally405
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

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.

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. TRACE and CONNECT are always rejected regardless of this list.
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

On Free Pascal, TMethodType does not include mtOptions. OPTIONS requests arrive as mtAny (unrecognised method) and will be rejected with 405 if OPTIONS is not also listed in AllowedMethods and the runtime can distinguish it from other unknown methods — which it cannot on FPC. If you require OPTIONS support under FPC + Indy, remove OPTIONS from the check by either clearing AllowedMethods (disables the method check entirely) or accepting the limitation.


License

MIT — see LICENSE.