Recent

Author Topic: AdvancedHTTPServer: A Go-style Web Server for Free Pascal  (Read 1511 times)

CynicRus

  • Jr. Member
  • **
  • Posts: 62
AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« on: January 17, 2026, 01:19:40 pm »
AdvancedHTTPServer: A Go-style Web Server for Free Pascal

Taking the opportunity to share my HTTP/HTTPS server with the community, inspired by the net/http package from Go. I have always liked the Go paradigm for web application development. About a year ago, I wanted to create something similar but in my favorite language, Pascal.

AdvancedHTTPServer is a lightweight, high-performance HTTP/1.1 server written in Free Pascal, inspired by the philosophy of the Go standard library.

Main Goal:
To provide the simplest, most pleasant API possible for creating web apps in Pascal, close in spirit to the minimalist and productive approach of the Go standard library.

Key Features:
  • Minimal boilerplate code
  • "Handler first" approach (THandlerFunc)
  • Go-style middleware support
  • Clear separation of concerns
  • Good performance "out of the box" (epoll on Linux / IOCP on Windows)
  • TLS support
  • Keep-alive and HTTP pipelining
  • Go-like coding style


Example Application:

Code: Pascal  [Select][+][-]
  1. program ahttpServDemo;
  2.  
  3. {$mode objfpc}{$H+}
  4. {$modeswitch functionreferences}
  5. {$modeswitch anonymousfunctions}
  6.  
  7. uses
  8.   {$IFDEF UNIX}cthreads,{$ENDIF}
  9.   Classes, SysUtils, CustApp, AdvancedHTTPServer, DateUtils;
  10.  
  11.   function LoggingMiddleware(Next: THandlerFunc): THandlerFunc;
  12.   begin
  13.     Result := procedure(W: TResponseWriter; R: TRequest)
  14.     var
  15.       StartTime: TDateTime;
  16.       Duration: int64;
  17.       Proto: string;
  18.     begin
  19.       StartTime := Now;
  20.       Proto := 'HTTPS';
  21.       if not R.TLS then Proto := 'HTTP';
  22.   WriteLn(FormatDateTime('[yyyy-mm-dd hh:nn:ss]', StartTime), ' ',
  23.   Proto, ' ', R.Method, ' ', R.Path, ' from ', R.RemoteAddr);
  24.  
  25.   Next(W, R);  // Call next handler
  26.  
  27.   Duration := MilliSecondsBetween(Now, StartTime);
  28.   WriteLn('  Completed in ', Duration, 'ms');
  29. end;
  30.  
  31.   end;
  32.  
  33.   function RecoveryMiddleware(Next: THandlerFunc): THandlerFunc;
  34.   begin
  35.     Result := procedure(W: TResponseWriter; R: TRequest)
  36.     begin
  37.       try
  38.         Next(W, R);
  39.       except
  40.         on E: Exception do
  41.         begin
  42.           WriteLn('PANIC: ', E.Message);
  43.           if not W.HeadersSent then
  44.           begin
  45.             W.Header.SetValue('Content-Type', 'text/plain');
  46.             W.WriteHeader(500);
  47.             W.Write('500 Internal Server Error'#13#10);
  48.           end;
  49.         end;
  50.       end;
  51.     end;
  52.   end;
  53.  
  54.   procedure RootHandler(W: TResponseWriter; R: TRequest);
  55.   begin
  56.     W.Header.SetValue('Content-Type', 'text/html; charset=utf-8');
  57.     W.Write('<html><body>');
  58.     W.Write('<h1>Welcome to Advanced FreePascal HTTP Server</h1>');
  59.     W.Write('<p>Protocol: ' + ifthen(R.TLS,'HTTPS','HTTP') + '</p>');
  60.     W.Write('<p>Path: ' + R.Path + '</p>');
  61.     W.Write('</body></html>');
  62.   end;
  63.  
  64.   procedure JSONHandler(W: TResponseWriter; R: TRequest);
  65.   begin
  66.     W.Header.SetValue('Content-Type', 'application/json');
  67.     W.Write('{"status":"ok","path":"' + R.Path + '"}');
  68.   end;
  69.  
  70. var
  71.   Srv: THTTPServer;
  72. begin
  73.   Srv := THTTPServer.Create;
  74.   try
  75.     Srv.Use(@RecoveryMiddleware);
  76.     Srv.Use(@LoggingMiddleware);
  77.     Srv.HandleFunc('/', @RootHandler);
  78.     Srv.HandleFunc('/api/test', @JSONHandler);
  79. // Start server
  80. Srv.ListenAndServe(':8080');
  81.  
  82.   finally
  83.     Srv.Free;
  84.   end;
  85. end.
  86.  


Download:
GitHub  Repository

Details:
  • License: BSD-3-Clause
  • Supported Platforms: Windows / Linux


Generating a test certificate for TLS:
Code: [Select]
./openssl.exe req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes
« Last Edit: January 17, 2026, 01:23:08 pm by CynicRus »

ron.dunn

  • New Member
  • *
  • Posts: 28
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #1 on: January 17, 2026, 11:22:11 pm »
Can you expand on this comment from the Github page?

"Note: Like Go, it’s not multi-core by default. For more cores, run multiple instances behind a load balancer."

Does this mean it has a concurrency of 1?

CynicRus

  • Jr. Member
  • **
  • Posts: 62
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #2 on: January 18, 2026, 12:04:59 am »
Can you expand on this comment from the Github page?

"Note: Like Go, it’s not multi-core by default. For more cores, run multiple instances behind a load balancer."

Does this mean it has a concurrency of 1?

This means that it currently doesn't scale automatically based on the number of cpu cores, so if the load is tens of thousands of concurrent connections like > 10k, manual scaling will be necessary. Scaling will be added later, but that's a long-term prospect.

Boleeman

  • Hero Member
  • *****
  • Posts: 1106
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #3 on: January 18, 2026, 05:02:18 am »
Not really related to AdvancedHTTPServer,

Thanks CynicRus for doing PlutoVG-bindings-for-Lazarus-Delphi at
https://github.com/CynicRus/PlutoVG-bindings-for-Lazarus-Delphi

I played around with it last year with Lazarus 32Bit and 64Bit compiles.

Nice.

egsuh

  • Hero Member
  • *****
  • Posts: 1738
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #4 on: January 18, 2026, 10:48:13 am »
What is the core benefit of this compared with original Lazarus CGI/FCGI (and of course HTTP server) approach?

CynicRus

  • Jr. Member
  • **
  • Posts: 62
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #5 on: January 18, 2026, 12:48:43 pm »
What is the core benefit of this compared with original Lazarus CGI/FCGI (and of course HTTP server) approach?

The core benefit is the transition from a blocking/process model to a true asynchronous, event-driven architecture at the OS level. This provides an order of magnitude improvement in performance and scalability compared to classic CGI/FCGI solutions like Lazarus and even most embedded servers with same model.

The features already implemented are almost completely analogous to Nginx/Go net/http, but in Free Pascal. A few key features are missing (like load balancing across cores, etc.), but these will be implemented in the future.

Thaddy

  • Hero Member
  • *****
  • Posts: 18729
  • To Europe: simply sell USA bonds: dollar collapses
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #6 on: January 19, 2026, 10:58:25 am »
FCGI IS already an asynchronous solution. Single process/multiple connections.
« Last Edit: January 19, 2026, 12:51:34 pm by Thaddy »
If Europe sells their USA bonds the USD will collapse. Europe can affort that given average state debts. The USA can't affort that. Just an advice...

CynicRus

  • Jr. Member
  • **
  • Posts: 62
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #7 on: January 20, 2026, 12:59:07 am »
Hello again) Now implemented the middleware router - AdvancedHTTPRouter. Also few fixes, like ip:port params on the constructor, and accept in ipv6 mode:)

AdvancedHTTPRouter is a high-performance, lightweight HTTP router designed specifically to integrate with your AdvancedHTTPServer. It provides a modern, structured way to define routes, handle requests, and organize application logic, inspired by popular frameworks like Gin (Go) or Echo.

Key Features

Efficient Routing with Radix Tree
Uses a  radix tree (trie) for route matching. This ensures very fast lookups, even with hundreds or thousands of routes. It handles:
• Static paths:
Code: [Select]
/users/list• Named parameters:
Code: [Select]
/users/:id → access via
Code: [Select]
ctx.Param('id')• Wildcard catch-all:
Code: [Select]
/files/*path → captures everything after

HTTP Method Support
Dedicated methods for all standard verbs:
Code: [Select]
GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Code: [Select]
Any() for routes that respond to all methods
• Automatic fallback for HEAD to GET handlers if no explicit HEAD route exists

Middleware System
• Global middleware via
Code: [Select]
router.Use(...)• Per-group middleware
• Middleware can call
Code: [Select]
ctx.Next() to continue the chain or
Code: [Select]
ctx.Abort() to stop it
• Middleware and handlers form a chain — you can attach multiple handlers per route

Route Grouping
Powerful grouping with prefixes and nested groups:

Example structure:
Code: [Select]
v1 := router.Group("/api/v1")
{
v1.GET("/users", getUsers)
v1.POST("/users", createUser)
}

Sample code:
Code: [Select]
var
  AppState : TAppState;
  Server: THTTPServer;
  Router: THTTPRouter;
  APIGroup: THTTPRouterGroup;
begin
  AppState := TAppState.Create;
  try
    Server := THTTPServer.Create;
    try
      Server.MaxHeaderBytes := 65536;
      Server.MaxBodyBytes := 10 * 1024 * 1024; // 10MB

      Router := THTTPRouter.Create(Server);
      try
        // global middleware (logging)
        Router.Use(
          procedure(C: TObject)
          begin
            WriteLn('[LOG] ', THTTPRouterContext(C).R.Method, ' ', THTTPRouterContext(C).R.URL);
            THTTPRouterContext(C).Next;
          end
        );

        // Main page - get SPA
        Router.GET('/', [@StaticHandler]);

        // API group
        APIGroup := Router.Group('/api') as THTTPRouterGroup;
        APIGroup.Use(@AuthMiddleware); // защищаем весь API

        // POST /api/users
        APIGroup.POST('/users',
          [
            procedure(C: TObject)
            var
              Body: string;
              JSON: TJSONObject;
              Name, Email: string;
              ID: integer;
            begin
              Body := THTTPRouterContext(C).R.Body;
              if Body = '' then
              begin
                THTTPRouterContext(C).Text(400, 'Empty body');
                Exit;
              end;
              JSON := GetJSON(Body) as TJSONObject;
              try
                Name := JSON.Get('name', '');
                Email := JSON.Get('email', '');
                if (Name = '') or (Email = '') then
                begin
                  THTTPRouterContext(C).Text(400, 'Name and email required');
                  Exit;
                end;
                ID := AppState.AddUser(Name, Email);
                JSON.Integers['id'] := ID;
                THTTPRouterContext(C).JSON(201, JSON);
              finally
                JSON.Free;
              end;
            end
          ]
        );

        // GET /api/users/:id
        APIGroup.GET('/users/:id',
          [
            procedure(C: TObject)
            var
              ID: integer;
              User: TJSONObject;
            begin
              ID := StrToIntDef(THTTPRouterContext(C).Param('id'), -1);
              if ID <= 0 then
              begin
                THTTPRouterContext(C).Text(400, 'Invalid ID');
                Exit;
              end;
              User := AppState.GetUser(ID);
              if not Assigned(User) then
                THTTPRouterContext(C).Text(404, 'User not found')
              else
              begin
                THTTPRouterContext(C).JSON(200, User);
                User.Free;
              end;
            end
          ]
        );

        // PUT /api/users/:id
        APIGroup.PUT('/users/:id',
          [
            procedure(C: TObject)
            var
              ID: integer;
              Body: string;
              JSON: TJSONObject;
              Name, Email: string;
            begin
              ID := StrToIntDef(THTTPRouterContext(C).Param('id'), -1);
              if ID <= 0 then
              begin
                THTTPRouterContext(C).Text(400, 'Invalid ID');
                Exit;
              end;
              if not Assigned(AppState.GetUser(ID)) then
              begin
                THTTPRouterContext(C).Text(404, 'User not found');
                Exit;
              end;
              Body := THTTPRouterContext(C).R.Body;
              JSON := GetJSON(Body) as TJSONObject;
              try
                Name := JSON.Get('name', '');
                Email := JSON.Get('email', '');
                AppState.UpdateUser(ID, Name, Email);
                THTTPRouterContext(C).Text(204, '');
              finally
                JSON.Free;
              end;
            end
          ]
        );

        // DELETE /api/users/:id
        APIGroup.DELETE('/users/:id',
          [
            procedure(C: TObject)
            var
              ID: integer;
            begin
              ID := StrToIntDef(THTTPRouterContext(C).Param('id'), -1);
              if ID <= 0 then
              begin
                THTTPRouterContext(C).Text(400, 'Invalid ID');
                Exit;
              end;
              AppState.DeleteUser(ID);
              THTTPRouterContext(C).Text(204, '');
            end
          ]
        );

        // GET /api/users
        APIGroup.GET('/users',
          [
            procedure(C: TObject)
            begin
              THTTPRouterContext(C).JSON(200, AppState.ListUsers);
            end
          ]
        );

        // Static
        Router.Any('/public/*filepath',
          [
            procedure(C: TObject)
            var
              Path: string;
              FullPath: string;
            begin
              Path := THTTPRouterContext(C).Param('filepath');
              FullPath := 'public/' + StringReplace(Path, '..', '', [rfReplaceAll]);
              if FileExists(FullPath) then
                ServeFile(THTTPRouterContext(C).W, THTTPRouterContext(C).R, FullPath)
              else
                THTTPRouterContext(C).Text(404, 'Not Found');
            end
          ]
        );

        Router.Mount;
        WriteLn('Starting server on http://localhost:8080');
        Server.ListenAndServe(':8080');
      finally
        Router.Free;
      end;
    finally
      Server.Free;
    end;
  finally
    AppState.Free;
  end;

egsuh

  • Hero Member
  • *****
  • Posts: 1738
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #8 on: January 20, 2026, 06:25:59 am »
I'm not a skilled programmer. Rather I make programs for use. So I have a few questions on this paradigm. Some (some among a few?) might look silly, but I'd like to confirm.


1. It is HTTP server anyway. So, I assume that it should be running to server web requests. Am I right?
2. So, it does not need IIS, nginx, Apache, etc.
3. And it uses similar method to FastCGI in processing web requests. Database link once loaded does not need to be loaded again for every request.
4. It does not based on current Lazarus fpweb, which means it does not need TFPWebModules.

I have a web server running. Currently it is in CGI.

5. So, how much effort is needed to change CGI way to your way?
6. FCGI has some problem. Are you sure that there are no problem?
7. Can't I run it on IIS or NGINX? (not sure doing these has any merits --- possibly routings.)


If your approach does not take much resources and stable enough, I'd like to move to your framework.


« Last Edit: January 20, 2026, 06:32:26 am by egsuh »

Thaddy

  • Hero Member
  • *****
  • Posts: 18729
  • To Europe: simply sell USA bonds: dollar collapses
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #9 on: January 20, 2026, 11:54:13 am »
@CynicRus and @egsuh
The code is good, no criticsm, apart from not finished.
But when you change your CGI to FCGI it is already asynchronous - because fcgi is asynchronous as in single process - and neither apache nor nginx will complain.
What he has done is write an nginx like server software in FreePascal.
I have some doubts - even after testing - but the code works as advertised so I would recommend experimenting with it. It is basically nginx in pascal...
Since it does not really SCALE - yet - be aware that I did not do any stress tests.

But the code is otherwise OK and working.

Right now I have to manually distribute the code over 10 processes over 10 cores ( I have 16/32 available for testing) which seems to defeat the original description as non-blocking..
Keep up the good work, though.

I would like to be able to cluster more sites from a local network to external and that seems not yet possible  without manually allocating individual processes spawned over multiple cores.
The latter is easy with nginx.
« Last Edit: January 20, 2026, 12:07:15 pm by Thaddy »
If Europe sells their USA bonds the USD will collapse. Europe can affort that given average state debts. The USA can't affort that. Just an advice...

gidesa

  • Full Member
  • ***
  • Posts: 213
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #10 on: January 20, 2026, 01:29:16 pm »
2. So, it does not need IIS, nginx, Apache, etc.

Apache, Nginx, IIS are software thoroughly tested, field-verified, and with dozens of addons. So I will not compare directly.
Apart this, some time ago I was curious of learning how today applications are exposed to web.
Seems that old standards/protocols, as CGI, FastCGI, Isapi, Apache modules, today are superseded by so called
"reverse proxy": a front-end Http server (the reverse proxy), as Apache, Nginx, IIS, is directly exposed to web. Then the application has
his private Http server. The  reverse proxy receives request for application and send them to the private server using Http protocol; private
server then returns the response. That is, reverse proxy communicate with applications entirely on Http standard protocol, instead that using
FastCgi, Isapi, etc.
I have done some test with Apache, activating a reverse proxy is really matter of few lines in configuration file.
See for example: https://www.reddit.com/r/rust/comments/1ldqavv/why_doesnt_rust_web_dev_uses_fastcgi_wouldnt_it/

CynicRus

  • Jr. Member
  • **
  • Posts: 62
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #11 on: January 20, 2026, 05:06:13 pm »
@Thaddy, Thank you!

Well, initial multi-core scaling implemented now)

Changelog:
  • Multi-worker on Windows / multi-threaded accept loop on Linux
       
    • Added
Code: [Select]
TEpollWorkerThread — each worker has its own listening socket + dedicated epoll instance.
   
  • Added
    Code: [Select]
    TIOCPLoopThread — each worker has its own iocp loop now.
       
  • Code: [Select]
    SO_REUSEPORT is now used → allows true multi-threaded accept scaling.
       
  • Code: [Select]
    WorkerCount
      property controls number of accept + epoll threads (default = 1).
         

  • Real client IP support behind reverse proxies / load balancers
       
    • New properties:
    Code: [Select]
    BehindProxy,
    Code: [Select]
    TrustedProxyCIDRs (CIDR notation, IPv4 + IPv6).
       
  • Parsing logic for
    Code: [Select]
    Forwarded,
    Code: [Select]
    X-Forwarded-For,
    Code: [Select]
    X-Real-IP headers.
       
  • Code: [Select]
    RemoteAddr now contains real client IP when connection comes from a trusted proxy.
       
  • Unified Memory BIO usage on Linux and Windows
       
    • Both platforms now use
      Code: [Select]
      BIO_new(BIO_s_mem) for read/write separation in TLS mode.
         
    • Removed direct socket I/O during handshake → cleaner flow.

  • Graceful shutdown improvements
       
    • Code: [Select]
      Shutdown method now properly waits for connections (with timeout).
         
    • Code: [Select]
      WaitForConnections logic.
         
    • Code: [Select]
      FShutdownEvent (RTL event) for waking up loops.
         
  • Better per-state timeout handling
       
    • Code: [Select]
      HeaderReadTimeout (default 10 s).
         
    • Code: [Select]
      BodyReadTimeout (default 60 s).
         
    • Code: [Select]
      ConnectionTimeout (general keep-alive).
         
  • Behavioral & Bugfix Changes
    • Much more careful connection lifecycle management.
    • Code: [Select]
      CloseConnection is now thread-safe (locked list removal).
    • Code: [Select]
      CheckConnectionTimeouts collects connections to close outside the lock
    • Code: [Select]
      ParseRequestLine / header parsing correctly handles proxy headers only when trusted.
    • Improved pipeline / keep-alive logic in
      Code: [Select]
      ProcessRequestsFromBuffer.
    • Many small fixes in epoll / IOCP read-write handling.

  • Code Quality & Portability
    • Unified Windows/Linux TLS BIO flow → easier to maintain.
    • Better IPv6 literal normalization & parsing helpers.
    • Added
      Code: [Select]
      NormalizeIPLiteral,
      Code: [Select]
      IPInCIDR,
      Code: [Select]
      ParseCIDR
          functions.
        • Removed redundant code paths.
        • Improved error logging in critical places.

      • Removed / Deprecated
        • Old single-threaded epoll loop on Linux (replaced by per-worker model).
        • Some direct socket writes during TLS handshake (replaced with BIO flow).
      Code sample for behind proxy:
      Code: [Select]
      var S: THTTPServer;
      begin
        S := THTTPServer.Create;
        S.BehindProxy := True;

        // WE TRUST ONLY nginx on the same machine (IPv4+IPv6 loopback) and, for example, the local docker subnet
        S.TrustedProxyCIDRs.Add('127.0.0.1/32');
        S.TrustedProxyCIDRs.Add('::1/128');
        S.TrustedProxyCIDRs.Add('172.16.0.0/12');

        S.UseForwardedHeaders := True;
        S.UseXForwardedFor := True;
        S.UseXRealIP := True;

        S.ListenAndServe('0.0.0.0:8080');
      end;
« Last Edit: January 20, 2026, 05:17:39 pm by CynicRus »

egsuh

  • Hero Member
  • *****
  • Posts: 1738
Re: AdvancedHTTPServer: A Go-style Web Server for Free Pascal
« Reply #12 on: January 21, 2026, 01:44:33 am »
@Thaddy

Quote
But when you change your CGI to FCGI it is already asynchronous - because fcgi is asynchronous as in single process - and neither apache nor nginx will complain.

I do not have any complaint on FCGI per se. I do not like mentioning repeatedly, but the FPC FCGI has some problem (HTTPServer as well). It downs sometimes, even though very infrequently. Simply I'd like to know there are any other options --- like converting to Delphi & ISAPI. For now I do not have many users so CGI is good enough.

 

TinyPortal © 2005-2018