Skip to main content

Overview

CGI (Common Gateway Interface) deployment allows you to run Horse applications as standard CGI programs. This is the most portable web deployment method, supported by virtually all web servers including Apache, Nginx, IIS, and others.
CGI deployment requires the HORSE_CGI conditional define. A new process is spawned for each request, making it less efficient than other deployment methods but more portable.

When to Use CGI Deployment

  • Maximum Portability: Works with any web server supporting CGI
  • Shared Hosting: Common on shared hosting environments
  • Simple Deployment: Minimal server configuration required
  • Process Isolation: Each request runs in isolated process
  • Legacy Systems: Integration with older web server setups
  • Development/Testing: Quick setup for testing
CGI spawns a new process for each request, making it slower than FastCGI, ISAPI, or Apache modules. Use for low-traffic applications or consider FastCGI for better performance.

Required Conditional Define

1

Open Project Options

Go to Project → Options → Delphi Compiler → Compiling
2

Add Conditional Define

Add: HORSE_CGI
3

Set Application Type

Ensure project is a console application: {$APPTYPE CONSOLE}
4

Rebuild Project

Clean and rebuild your project

Complete CGI Example

program CGI;

{$APPTYPE CONSOLE}
{$R *.res}

uses Horse;

begin
  // Need to set "HORSE_CGI" compilation directive

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

  THorse.Listen;
end.

Advanced CGI Application

program AdvancedCGI;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.SysUtils,
  System.Classes,
  System.JSON,
  Horse,
  Horse.Jhonson,
  Horse.CORS;

begin
  // Enable CORS
  THorse.Use(CORS);
  
  // Enable JSON middleware
  THorse.Use(Jhonson);

  // Routes
  THorse.Get('/health',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin
      Res.Send('{"status": "healthy", "method": "CGI"}');
    end);

  THorse.Get('/api/info',
    procedure(Req: THorseRequest; Res: THorseResponse)
    var
      Info: TJSONObject;
    begin
      Info := TJSONObject.Create;
      try
        Info.AddPair('server', 'Horse CGI');
        Info.AddPair('method', 'CGI/1.1');
        Info.AddPair('timestamp', DateTimeToStr(Now));
        
        // CGI environment variables
        Info.AddPair('query_string', GetEnvironmentVariable('QUERY_STRING'));
        Info.AddPair('request_method', GetEnvironmentVariable('REQUEST_METHOD'));
        Info.AddPair('remote_addr', GetEnvironmentVariable('REMOTE_ADDR'));
        
        Res.Send<TJSONObject>(Info);
      finally
        Info.Free;
      end;
    end);

  THorse.Get('/api/users',
    procedure(Req: THorseRequest; Res: THorseResponse)
    var
      Users: TJSONArray;
      User: TJSONObject;
    begin
      Users := TJSONArray.Create;
      try
        User := TJSONObject.Create;
        User.AddPair('id', '1');
        User.AddPair('name', 'John Doe');
        Users.AddElement(User);
        
        User := TJSONObject.Create;
        User.AddPair('id', '2');
        User.AddPair('name', 'Jane Smith');
        Users.AddElement(User);
        
        Res.Send<TJSONArray>(Users);
      finally
        Users.Free;
      end;
    end);

  THorse.Post('/api/data',
    procedure(Req: THorseRequest; Res: THorseResponse)
    var
      Body: TJSONObject;
      Response: TJSONObject;
    begin
      Body := Req.Body<TJSONObject>;
      
      Response := TJSONObject.Create;
      try
        Response.AddPair('received', 'true');
        Response.AddPair('timestamp', DateTimeToStr(Now));
        
        Res.Status(201).Send<TJSONObject>(Response);
      finally
        Response.Free;
      end;
    end);

  THorse.Listen;
end.

Web Server Configuration

Apache Configuration

# Enable CGI module
LoadModule cgi_module modules/mod_cgi.so

# Configure CGI directory
ScriptAlias /api/ "/var/www/cgi-bin/"

<Directory "/var/www/cgi-bin">
    Options +ExecCGI
    AddHandler cgi-script .cgi
    Require all granted
</Directory>

# Or configure specific file
<Files "cgi">
    SetHandler cgi-script
</Files>
Deploy your application:
# Copy CGI executable
sudo cp CGI /var/www/cgi-bin/api.cgi

# Set executable permission
sudo chmod +x /var/www/cgi-bin/api.cgi

# Set ownership
sudo chown www-data:www-data /var/www/cgi-bin/api.cgi

# Test
curl http://localhost/api/api.cgi/ping

Nginx Configuration

server {
    listen 80;
    server_name example.com;
    
    location /api {
        # CGI configuration
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/cgi-bin/api.cgi$fastcgi_path_info;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_pass unix:/var/run/fcgiwrap.socket;
    }
}
Nginx doesn’t support CGI natively. Use fcgiwrap to enable CGI support:
sudo apt-get install fcgiwrap
sudo systemctl enable fcgiwrap
sudo systemctl start fcgiwrap

IIS Configuration

1

Enable CGI Feature

Server ManagerAdd Roles and FeaturesWeb Server (IIS)Application Development → ✅ CGI
2

Copy Executable

copy CGI.exe "C:\inetpub\wwwroot\api\"
3

Configure Handler Mapping

IIS Manager → Site → Handler MappingsAdd Script Map
  • Request path: *.cgi
  • Executable: C:\inetpub\wwwroot\api\CGI.exe
  • Name: Horse_CGI
4

Test

Navigate to: http://localhost/api/CGI.exe/ping

URL Rewriting for Clean URLs

Apache .htaccess

RewriteEngine On
RewriteBase /api

# Remove .cgi extension from URL
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ api.cgi/$1 [L,QSA]
This allows:
# Instead of:
http://localhost/api/api.cgi/ping

# You can use:
http://localhost/api/ping

Nginx Rewrite

location /api {
    rewrite ^/api/(.*)$ /cgi-bin/api.cgi/$1 last;
}

CGI Environment Variables

Access standard CGI environment variables:
THorse.Get('/env',
  procedure(Req: THorseRequest; Res: THorseResponse)
  var
    Env: TJSONObject;
  begin
    Env := TJSONObject.Create;
    try
      // Request information
      Env.AddPair('REQUEST_METHOD', GetEnvironmentVariable('REQUEST_METHOD'));
      Env.AddPair('QUERY_STRING', GetEnvironmentVariable('QUERY_STRING'));
      Env.AddPair('CONTENT_TYPE', GetEnvironmentVariable('CONTENT_TYPE'));
      Env.AddPair('CONTENT_LENGTH', GetEnvironmentVariable('CONTENT_LENGTH'));
      
      // Server information
      Env.AddPair('SERVER_SOFTWARE', GetEnvironmentVariable('SERVER_SOFTWARE'));
      Env.AddPair('SERVER_NAME', GetEnvironmentVariable('SERVER_NAME'));
      Env.AddPair('SERVER_PORT', GetEnvironmentVariable('SERVER_PORT'));
      Env.AddPair('SERVER_PROTOCOL', GetEnvironmentVariable('SERVER_PROTOCOL'));
      
      // Client information
      Env.AddPair('REMOTE_ADDR', GetEnvironmentVariable('REMOTE_ADDR'));
      Env.AddPair('REMOTE_HOST', GetEnvironmentVariable('REMOTE_HOST'));
      Env.AddPair('HTTP_USER_AGENT', GetEnvironmentVariable('HTTP_USER_AGENT'));
      
      // Path information
      Env.AddPair('SCRIPT_NAME', GetEnvironmentVariable('SCRIPT_NAME'));
      Env.AddPair('PATH_INFO', GetEnvironmentVariable('PATH_INFO'));
      
      Res.Send<TJSONObject>(Env);
    finally
      Env.Free;
    end;
  end);

Performance Considerations

Process Overhead

CGI spawns a new process for each request. This includes:
  • Loading the executable
  • Initializing the runtime
  • Connecting to databases
  • Processing the request
  • Cleaning up resources
For high-traffic applications, consider:
  • FastCGI deployment (using Lazarus)
  • Apache module deployment
  • ISAPI deployment (Windows)
  • Console with reverse proxy

Optimization Tips

begin
  // Minimize initialization code
  // Configure routes inline
  
  THorse.Get('/quick',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin
      // Quick response
      Res.Send('OK');
    end);
  
  // Avoid expensive operations in initialization
  // Don't open persistent database connections
  
  THorse.Listen;
end.

Debugging CGI Applications

Enable Error Output

begin
  try
    // Your routes
    THorse.Get('/test',
      procedure(Req: THorseRequest; Res: THorseResponse)
      begin
        Res.Send('Test');
      end);
    
    THorse.Listen;
  except
    on E: Exception do
    begin
      // Output error to stderr
      Writeln(ErrOutput, 'Content-Type: text/plain');
      Writeln(ErrOutput, '');
      Writeln(ErrOutput, 'Error: ' + E.Message);
      Writeln(ErrOutput, E.StackTrace);
    end;
  end;
end.

Check Web Server Logs

# Apache error log
tail -f /var/log/apache2/error.log

# Nginx error log
tail -f /var/log/nginx/error.log

# IIS logs
type C:\inetpub\logs\LogFiles\W3SVC1\*.log

Test from Command Line

# Set CGI environment variables manually
export REQUEST_METHOD=GET
export QUERY_STRING="id=123"
export SCRIPT_NAME="/api/test.cgi"
export PATH_INFO="/ping"

# Run CGI script
./CGI

Security Considerations

Input Validation

THorse.Post('/api/users',
  procedure(Req: THorseRequest; Res: THorseResponse)
  var
    Body: TJSONObject;
  begin
    try
      Body := Req.Body<TJSONObject>;
      
      // Validate input
      if not Body.TryGetValue('name', EmptyStr).IsEmpty then
      begin
        // Process valid input
        Res.Send('{"status": "success"}');
      end
      else
      begin
        Res.Status(400).Send('{"error": "name required"}');
      end;
    except
      on E: Exception do
        Res.Status(500).Send('{"error": "Invalid JSON"}');
    end;
  end);

File Permissions (Linux)

# Set minimal required permissions
chmod 755 api.cgi

# Ensure proper ownership
chown www-data:www-data api.cgi

# Don't make world-writable
chmod 644 config.ini  # Read-only for others

Troubleshooting

Internal Server Error (500)

1

Check Executable Permission

ls -la /var/www/cgi-bin/api.cgi
chmod +x /var/www/cgi-bin/api.cgi
2

Verify Conditional Define

Ensure HORSE_CGI is set in project options
3

Check Dependencies

Ensure all required libraries are available
4

Review Error Logs

tail -f /var/log/apache2/error.log

No Output / Blank Page

// Ensure headers are sent
THorse.Get('/test',
  procedure(Req: THorseRequest; Res: THorseResponse)
  begin
    Res.ContentType('text/plain');
    Res.Send('Hello');
  end);

Permission Denied

# SELinux (if enabled)
sudo setsebool -P httpd_enable_cgi 1
sudo chcon -t httpd_sys_script_exec_t /var/www/cgi-bin/api.cgi

# AppArmor
sudo aa-complain /usr/sbin/apache2

Next Steps

Console Deployment

Deploy as standalone console application

Apache Module

Better performance with Apache module

Build docs developers (and LLMs) love