24 min read

Every time you open a web page, send an email, stream a video, or check the weather on your phone, you are using networked software. Two programs — a client that initiates the request and a server that fulfills it — communicate over a network using...

Learning Objectives

  • Explain the TCP/IP model and the client-server communication pattern
  • Build TCP client and server programs using Free Pascal's socket units
  • Make HTTP GET and POST requests using fphttpclient
  • Consume REST APIs by parsing JSON responses from HTTP requests
  • Build a simple HTTP server (MicroServe) that handles routes and returns responses
  • Understand WebSocket basics and when to use them
  • Add REST API synchronization to PennyWise

Chapter 35: Networking and Internet Programming: Sockets, HTTP, and REST Clients

"The network is the computer." — John Gage, Sun Microsystems


35.1 Network Programming Fundamentals

Every time you open a web page, send an email, stream a video, or check the weather on your phone, you are using networked software. Two programs — a client that initiates the request and a server that fulfills it — communicate over a network using agreed-upon rules called protocols. Understanding how this works at the socket level is one of the most valuable skills a programmer can acquire, because it demystifies everything from web browsers to database connections to cloud services.

This chapter teaches you to build networked Pascal programs from the ground up. We start with raw TCP sockets, move to HTTP clients, consume REST APIs, and culminate in building MicroServe — a working HTTP server written entirely in Pascal. By the end, PennyWise will be able to synchronize its expense data with a remote server via REST calls.

The TCP/IP Model

The internet runs on a four-layer model:

Layer Purpose Example Protocols
Application User-facing services HTTP, HTTPS, FTP, SMTP, DNS
Transport Reliable delivery between programs TCP, UDP
Internet Routing between networks IP (IPv4, IPv6)
Link Physical transmission Ethernet, Wi-Fi

As application programmers, we work primarily at the top two layers. The transport layer gives us two main choices:

TCP (Transmission Control Protocol) provides reliable, ordered, connection-oriented communication. Data is guaranteed to arrive, in order, without duplicates. If a packet is lost, TCP automatically retransmits it. This is what web browsers, email clients, and database connections use.

UDP (User Datagram Protocol) provides unreliable, connectionless communication. Packets might arrive out of order, be duplicated, or not arrive at all. But it is faster because there is no connection setup and no retransmission overhead. This is what video streaming, online gaming, and DNS lookups use.

For this chapter, we focus on TCP — it is more common, more predictable, and more appropriate for the applications we are building.

IP Addresses and Ports

Every device on a network has an IP address — a numeric identifier. IPv4 addresses look like 192.168.1.100 (four numbers from 0-255). IPv6 addresses look like 2001:0db8:85a3::8a2e:0370:7334 (eight groups of hexadecimal digits). The special address 127.0.0.1 (or localhost) always refers to the local machine — useful for testing.

A port is a 16-bit number (0-65535) that identifies a specific service on a machine. Think of the IP address as a building's street address and the port as an apartment number. Web servers conventionally use port 80 (HTTP) and 443 (HTTPS). Ports below 1024 are "well-known" and typically require administrator privileges. For development and testing, use ports above 1024 — 8080, 9090, and 3000 are common choices.

The combination of IP address and port uniquely identifies a network endpoint. When you type http://example.com:8080/api/data in a browser, DNS resolves example.com to an IP address, and the browser connects to that IP on port 8080.

The Client-Server Model

The client-server model is the foundation of networked computing:

  1. A server starts and listens on a specific port (a 16-bit number from 0 to 65535)
  2. A client connects to the server's IP address and port
  3. Once connected, both sides can send and receive data
  4. Either side can close the connection

Common ports: 80 (HTTP), 443 (HTTPS), 21 (FTP), 22 (SSH), 25 (SMTP), 3306 (MySQL), 5432 (PostgreSQL).

Sockets: The Programming Interface

A socket is the programming abstraction for network communication. Think of it as a file handle for the network — you open it, read from it, write to it, and close it. Just as file I/O uses AssignFile, Read, Write, and CloseFile, network I/O uses socket creation, Send, Recv, and socket closure.

💡 Intuition: Sockets as Phone Calls A TCP socket connection is like a phone call. The server is like a business with a published phone number (IP + port) that keeps listening for calls. The client dials the number. Once connected, both sides can talk (send data) and listen (receive data). Either side can hang up (close the connection). The connection is full-duplex — both sides can talk simultaneously.


35.2 TCP Sockets in Free Pascal

Free Pascal provides several units for socket programming. We will use the ssockets unit (part of the standard library) for TCP communication, as it provides a clean, object-oriented API that works on all platforms.

Key Classes

Class Purpose
TInetSocket TCP client socket — connects to a server
TInetServer TCP server socket — listens for connections
TSocketStream Stream wrapper for reading/writing socket data

Creating a TCP Client

The simplest TCP client connects to a server, sends a message, and reads the response:

program SimpleTCPClient;

{$mode objfpc}{$H+}

uses
  SysUtils, ssockets;

var
  Socket: TInetSocket;
  Buffer: array[0..4095] of Byte;
  BytesRead: Integer;
  Request: string;
begin
  WriteLn('Connecting to example.com:80...');

  Socket := TInetSocket.Create('example.com', 80);
  try
    { Send an HTTP request (raw TCP) }
    Request := 'GET / HTTP/1.0' + #13#10 +
               'Host: example.com' + #13#10 +
               #13#10;
    Socket.Write(Request[1], Length(Request));

    { Read the response }
    repeat
      BytesRead := Socket.Read(Buffer, SizeOf(Buffer));
      if BytesRead > 0 then
        Write(StringOf(Buffer, BytesRead));
    until BytesRead <= 0;
  finally
    Socket.Free;
  end;
end.

This is deliberately low-level to show what happens under the hood. In practice, you would use an HTTP client library (Section 35.4) rather than manually constructing HTTP requests.

Creating a TCP Server

A TCP server listens for incoming connections and handles each one:

program SimpleTCPServer;

{$mode objfpc}{$H+}

uses
  SysUtils, ssockets, Sockets;

type
  TEchoServer = class(TInetServer)
  public
    procedure HandleConnect(Sender: TObject; Data: TSocketStream);
  end;

procedure TEchoServer.HandleConnect(Sender: TObject; Data: TSocketStream);
var
  Buffer: array[0..1023] of Byte;
  BytesRead: Integer;
  Response: string;
begin
  WriteLn('Client connected');

  BytesRead := Data.Read(Buffer, SizeOf(Buffer));
  if BytesRead > 0 then
  begin
    Response := 'Echo: ' + Copy(PChar(@Buffer[0]), 1, BytesRead) + #13#10;
    Data.Write(Response[1], Length(Response));
    WriteLn('  Echoed ', BytesRead, ' bytes');
  end;

  WriteLn('Client disconnected');
end;

var
  Server: TEchoServer;
begin
  WriteLn('Starting echo server on port 7777...');
  Server := TEchoServer.Create('0.0.0.0', 7777);
  try
    Server.OnConnect := @Server.HandleConnect;
    WriteLn('Listening. Press Ctrl+C to stop.');
    Server.StartAccepting;
  finally
    Server.Free;
  end;
end.

The server creates a listening socket on port 7777. When a client connects, the OnConnect handler is called with a TSocketStream for communication. The handler reads the client's message, prepends "Echo: ", and sends it back.

A Complete TCP Echo Client

The simple client above sends a single request. Here is a more complete interactive echo client that maintains a conversation with the server:

program EchoClient;

{$mode objfpc}{$H+}

uses
  SysUtils, ssockets;

var
  Socket: TInetSocket;
  Buffer: array[0..4095] of Byte;
  BytesRead: Integer;
  Input, Response: string;
begin
  WriteLn('Connecting to echo server on localhost:7777...');

  try
    Socket := TInetSocket.Create('127.0.0.1', 7777);
    try
      WriteLn('Connected! Type messages (empty line to quit):');
      WriteLn;

      repeat
        Write('You: ');
        ReadLn(Input);
        if Input = '' then Break;

        { Send the message }
        Input := Input + #13#10;
        Socket.Write(Input[1], Length(Input));

        { Read the echo response }
        BytesRead := Socket.Read(Buffer, SizeOf(Buffer));
        if BytesRead > 0 then
        begin
          SetString(Response, PChar(@Buffer[0]), BytesRead);
          WriteLn('Server: ', Trim(Response));
        end
        else
        begin
          WriteLn('Server closed connection.');
          Break;
        end;
      until False;

      WriteLn('Disconnected.');
    finally
      Socket.Free;
    end;
  except
    on E: ESocketError do
      WriteLn('Connection failed: ', E.Message);
    on E: Exception do
      WriteLn('Error: ', E.Message);
  end;
end.

Notice the exception handling: ESocketError catches connection failures (server not running, wrong port, network unreachable). Without this try..except, the program would crash with an unhelpful error message. Always wrap network operations in exception handlers.

Understanding Socket Streams

When you create a TInetSocket or receive a TSocketStream in the server's OnConnect handler, you are working with a stream — a sequential byte sequence that you read from or write to. Sockets are bidirectional streams: you can both read and write on the same socket.

Key socket stream behaviors:

  • Read blocks until data arrives or the connection closes. If no data is available, Read waits. If the connection closes, Read returns 0.
  • Write blocks until the data is sent (or buffered by the OS). If the connection is broken, Write raises an exception.
  • Partial reads are normal. If you ask for 4096 bytes but only 100 have arrived, Read returns 100. You must loop if you need a specific amount.
  • No message boundaries. TCP is a byte stream, not a message stream. If you send "Hello" and "World" in two writes, the receiver might get "HelloWorld" in one read, or "He" and "lloWorld" in two reads. Your protocol must define how messages are delimited (newlines, length prefixes, etc.).

This last point is critical and catches many beginners. TCP guarantees that bytes arrive in order and without corruption, but it does not preserve message boundaries. If your protocol uses newline-terminated messages (like HTTP, SMTP, and our toy protocol), you must read until you find a newline — not assume that one Read call returns one complete message.

⚠️ Blocking I/O The simple server above is blocking: it handles one client at a time. While serving one client, all other clients wait. For a production server, you need either multithreading (Chapter 36) or asynchronous I/O. This server is for learning the fundamentals.


35.3 Building a TCP Client and Server

The echo server and simple client from Section 35.2 demonstrate the basic pattern, but they are too simple for real applications. A real client-server system needs a protocol — a set of rules that both sides agree on for structuring their communication. Without a protocol, the client and server are just throwing bytes at each other and hoping for the best.

Protocol design is a critical skill that many programmers never formally learn. A well-designed protocol is simple, unambiguous, extensible, and easy to debug. A poorly designed protocol causes endless headaches: messages that are misinterpreted, connections that hang, errors that are impossible to diagnose.

Let us build a more complete example: a TCP-based message exchange system where the client sends commands and the server responds.

Protocol Design

Before writing code, we define the protocol — the rules for communication:

CLIENT → SERVER: COMMAND arg1 arg2\r\n
SERVER → CLIENT: STATUS message\r\n

Commands:
  HELLO name       → 200 Welcome, name
  TIME             → 200 Current time is HH:MM:SS
  ECHO text        → 200 text
  QUIT             → 200 Goodbye
  (unknown)        → 400 Unknown command

This is a simple text-based protocol, similar to SMTP, FTP, and early HTTP. Each message is a single line terminated by \r\n (carriage return + line feed).

The Server

function HandleCommand(const Cmd: string): string;
var
  Parts: TStringList;
  Command: string;
begin
  Parts := TStringList.Create;
  try
    Parts.Delimiter := ' ';
    Parts.StrictDelimiter := True;
    Parts.DelimitedText := Cmd;

    if Parts.Count = 0 then
      Exit('400 Empty command');

    Command := UpperCase(Parts[0]);

    if Command = 'HELLO' then
    begin
      if Parts.Count > 1 then
        Result := '200 Welcome, ' + Parts[1]
      else
        Result := '200 Welcome, stranger';
    end
    else if Command = 'TIME' then
      Result := '200 Current time is ' + FormatDateTime('hh:nn:ss', Now)
    else if Command = 'ECHO' then
    begin
      if Parts.Count > 1 then
        Result := '200 ' + Copy(Cmd, 6, Length(Cmd))
      else
        Result := '200 (empty)';
    end
    else if Command = 'QUIT' then
      Result := '200 Goodbye'
    else
      Result := '400 Unknown command: ' + Command;
  finally
    Parts.Free;
  end;
end;

The Client

procedure RunClient(const Host: string; Port: Word);
var
  Socket: TInetSocket;
  Request, Response: string;
  Buffer: array[0..4095] of Byte;
  BytesRead: Integer;
begin
  Socket := TInetSocket.Create(Host, Port);
  try
    WriteLn('Connected to ', Host, ':', Port);
    WriteLn('Commands: HELLO name, TIME, ECHO text, QUIT');
    WriteLn;

    repeat
      Write('> ');
      ReadLn(Request);
      if Request = '' then Continue;

      { Send command }
      Request := Request + #13#10;
      Socket.Write(Request[1], Length(Request));

      { Read response }
      BytesRead := Socket.Read(Buffer, SizeOf(Buffer));
      if BytesRead > 0 then
      begin
        Response := Copy(PChar(@Buffer[0]), 1, BytesRead);
        WriteLn('< ', Trim(Response));
      end;
    until UpperCase(Trim(Request)) = 'QUIT'#13#10;
  finally
    Socket.Free;
  end;
end;

A Complete Multi-Command Server

Here is the complete server implementation that handles our protocol. It wraps the command handler in a connection loop that processes multiple commands per connection:

procedure HandleClientConnection(Data: TSocketStream);
var
  Buffer: array[0..4095] of Byte;
  BytesRead: Integer;
  Request, Response, Command: string;
  Done: Boolean;
begin
  WriteLn('Client connected from ', Data.RemoteAddress.Address);
  Done := False;

  while not Done do
  begin
    BytesRead := Data.Read(Buffer, SizeOf(Buffer));
    if BytesRead <= 0 then
    begin
      WriteLn('Client disconnected (connection closed)');
      Break;
    end;

    SetString(Request, PChar(@Buffer[0]), BytesRead);
    Request := Trim(Request);
    WriteLn('  Received: ', Request);

    Response := HandleCommand(Request) + #13#10;
    Data.Write(Response[1], Length(Response));
    WriteLn('  Sent: ', Trim(Response));

    { Check if client said QUIT }
    Command := UpperCase(Copy(Request, 1, 4));
    if Command = 'QUIT' then
    begin
      Done := True;
      WriteLn('Client requested disconnect');
    end;
  end;
end;

The server maintains the connection and processes commands in a loop until the client sends QUIT or closes the connection. This is how real servers work — FTP, SMTP, and IMAP all maintain long-lived connections with multiple commands per session.

Testing the Client and Server

To test, open two terminals:

Terminal 1 (Server):

$ ./SimpleTCPServer
Starting echo server on port 7777...
Listening. Press Ctrl+C to stop.
Client connected from 127.0.0.1
  Received: HELLO Rosa
  Sent: 200 Welcome, Rosa
  Received: TIME
  Sent: 200 Current time is 14:32:07
  Received: ECHO Hello from PennyWise!
  Sent: 200 Hello from PennyWise!
  Received: QUIT
  Sent: 200 Goodbye
Client requested disconnect

Terminal 2 (Client):

$ ./SimpleTCPClient
Connected to localhost:7777
Commands: HELLO name, TIME, ECHO text, QUIT

> HELLO Rosa
< 200 Welcome, Rosa
> TIME
< 200 Current time is 14:32:07
> ECHO Hello from PennyWise!
< 200 Hello from PennyWise!
> QUIT
< 200 Goodbye

This testing pattern — running server and client in separate terminals — is how you debug all client-server programs. For automated testing, you can run both in the same program using threads (Chapter 36).

📊 Real-World Protocols This toy protocol illustrates the pattern used by real protocols. HTTP uses GET /path HTTP/1.1\r\n with headers. SMTP uses HELO, MAIL FROM:, RCPT TO:, DATA, and QUIT. FTP uses USER, PASS, LIST, RETR, and QUIT. Understanding one text-based protocol helps you understand them all.


35.4 HTTP Clients

For most real-world networking tasks, you do not need raw sockets. You need HTTP — the protocol that powers the web. HTTP is built on top of TCP, so everything we learned about sockets is happening underneath, but fphttpclient handles the connection management, header parsing, content-length tracking, and keep-alive behavior for you.

The fphttpclient unit is part of Free Pascal's standard library — no third-party dependencies needed. It supports HTTP/1.0 and HTTP/1.1, GET and POST methods, custom headers, timeouts, redirects, and SSL/TLS (when linked with OpenSSL). For most API consumption needs, it is more than sufficient.

Why use fphttpclient instead of raw sockets? Because HTTP is deceptively complex. What looks like a simple text protocol has dozens of header fields, multiple transfer encodings (chunked, gzip), redirect chains (301, 302, 307), authentication schemes (Basic, Bearer, OAuth), and content negotiation. The HTTP client library handles all of this. You focus on your data; the library handles the protocol.

Simple GET Request

program HTTPGetDemo;

{$mode objfpc}{$H+}

uses
  SysUtils, fphttpclient;

var
  Client: TFPHTTPClient;
  Response: string;
begin
  Client := TFPHTTPClient.Create(nil);
  try
    { Simple GET request }
    Response := Client.Get('http://httpbin.org/ip');
    WriteLn('Response:');
    WriteLn(Response);
    WriteLn;
    WriteLn('HTTP Status: ', Client.ResponseStatusCode);
  finally
    Client.Free;
  end;
end.

The Get method sends an HTTP GET request and returns the response body as a string. The ResponseStatusCode property gives the HTTP status code (200 for success, 404 for not found, etc.).

POST Request with Data

procedure PostExample;
var
  Client: TFPHTTPClient;
  Response: string;
  PostData: TStringList;
begin
  Client := TFPHTTPClient.Create(nil);
  PostData := TStringList.Create;
  try
    { Form-encoded POST }
    PostData.Add('name=Rosa');
    PostData.Add('amount=85.50');
    PostData.Add('category=food');

    Response := Client.FormPost('http://httpbin.org/post', PostData);
    WriteLn('POST Response:');
    WriteLn(Response);
  finally
    PostData.Free;
    Client.Free;
  end;
end;

POST with JSON Body

For REST APIs, we often need to send JSON in the request body:

procedure PostJSON;
var
  Client: TFPHTTPClient;
  RequestBody: TStringStream;
  Response: TStringStream;
begin
  Client := TFPHTTPClient.Create(nil);
  RequestBody := TStringStream.Create(
    '{"description":"Groceries","amount":85.50,"category":"food"}'
  );
  Response := TStringStream.Create('');
  try
    Client.RequestBody := RequestBody;
    Client.AddHeader('Content-Type', 'application/json');

    Client.Post('http://httpbin.org/post', Response);
    WriteLn('Status: ', Client.ResponseStatusCode);
    WriteLn('Response: ', Response.DataString);
  finally
    Response.Free;
    RequestBody.Free;
    Client.Free;
  end;
end;

Setting Headers and Handling Errors

procedure RobustHTTPGet(const URL: string);
var
  Client: TFPHTTPClient;
  Response: string;
begin
  Client := TFPHTTPClient.Create(nil);
  try
    { Set custom headers }
    Client.AddHeader('User-Agent', 'PennyWise/3.4 (FreePascal)');
    Client.AddHeader('Accept', 'application/json');

    { Set timeout (milliseconds) }
    Client.IOTimeout := 10000;  { 10 seconds }
    Client.ConnectTimeout := 5000;  { 5 seconds }

    try
      Response := Client.Get(URL);
      WriteLn('Status: ', Client.ResponseStatusCode);

      case Client.ResponseStatusCode of
        200: WriteLn('Success: ', Response);
        301, 302: WriteLn('Redirect to: ',
          Client.GetHeader(Client.ResponseHeaders, 'Location'));
        404: WriteLn('Not found');
        500: WriteLn('Server error');
      else
        WriteLn('Unexpected status: ', Client.ResponseStatusCode);
      end;
    except
      on E: EHTTPClient do
        WriteLn('HTTP error: ', E.Message);
      on E: ESocketError do
        WriteLn('Network error: ', E.Message);
      on E: Exception do
        WriteLn('Error: ', E.Message);
    end;
  finally
    Client.Free;
  end;
end;

Downloading Files

A common task is downloading a file from the internet and saving it to disk:

procedure DownloadFile(const URL, LocalFilename: string);
var
  Client: TFPHTTPClient;
begin
  Client := TFPHTTPClient.Create(nil);
  try
    Client.IOTimeout := 30000;  { 30 seconds for large files }
    Client.AllowRedirect := True;
    try
      Client.Get(URL, LocalFilename);
      WriteLn('Downloaded: ', LocalFilename);
      WriteLn('Status: ', Client.ResponseStatusCode);
    except
      on E: EHTTPClient do
        WriteLn('Download failed: ', E.Message);
    end;
  finally
    Client.Free;
  end;
end;

The overloaded Get(URL, Filename) method writes the response directly to a file, which is more memory-efficient than loading the entire response into a string for large files.

HTTPS and TLS

All production web traffic should use HTTPS (HTTP over TLS) to prevent eavesdropping and tampering. TFPHTTPClient supports HTTPS when the OpenSSL libraries are available on the system.

On Windows, you need libeay32.dll and ssleay32.dll (OpenSSL 1.0) or libcrypto-1_1-x64.dll and libssl-1_1-x64.dll (OpenSSL 1.1) in your application directory or system PATH. On Linux, libssl.so and libcrypto.so are typically installed as part of the system.

To use HTTPS, add the openssl and opensslsockets units to your uses clause:

uses
  SysUtils, fphttpclient, openssl, opensslsockets;

begin
  InitSSLInterface;  { Initialize OpenSSL }

  with TFPHTTPClient.Create(nil) do
  try
    WriteLn(Get('https://api.github.com'));  { HTTPS works! }
  finally
    Free;
  end;
end.

If the OpenSSL libraries are not found, the HTTPS request will raise an exception. Your application should handle this gracefully — either by falling back to HTTP (for non-sensitive data) or by informing the user that SSL libraries need to be installed.

⚠️ Always Handle Network Errors Network operations can fail for reasons completely outside your control: the server is down, the network is unreliable, DNS resolution fails, the connection times out. Always wrap HTTP calls in try..except blocks and provide meaningful error messages.


35.5 Consuming REST APIs

REST (Representational State Transfer) APIs are the standard way modern applications exchange data over HTTP. A REST API exposes resources (expenses, users, budgets) at URL endpoints and uses HTTP methods (GET, POST, PUT, DELETE) for operations. Nearly every web service you can think of — Twitter, GitHub, Stripe, weather services, mapping services — provides a REST API.

The beauty of REST is its simplicity and uniformity. Once you know how to call one REST API, you know the pattern for all of them. The URL identifies the resource, the HTTP method identifies the operation, the request body (usually JSON) carries the data, and the response body (also JSON) carries the result. This uniformity means you can build a generic REST client class and reuse it across different services.

For PennyWise, a REST API lets Rosa sync her expenses with a server. The desktop application pushes new expenses via POST, pulls updates via GET, and the server stores everything in a database. Later, she can access her data from a phone, a web browser, or another computer — all through the same API.

REST Conventions

HTTP Method Purpose Example
GET Retrieve data GET /api/expenses → list of expenses
POST Create data POST /api/expenses → create new expense
PUT Update data PUT /api/expenses/42 → update expense #42
DELETE Delete data DELETE /api/expenses/42 → delete expense #42

Responses are typically JSON. Status codes indicate success (2xx), client errors (4xx), or server errors (5xx).

Building a REST Client

Here is a reusable REST client class for consuming a PennyWise API:

type
  TRESTClient = class
  private
    FBaseURL: string;
    FHTTPClient: TFPHTTPClient;
  public
    constructor Create(const BaseURL: string);
    destructor Destroy; override;
    function GetExpenses: TJSONArray;
    function GetExpense(ID: Integer): TJSONObject;
    function CreateExpense(const Desc: string; Amount: Double;
      const Category: string): TJSONObject;
    function DeleteExpense(ID: Integer): Boolean;
  end;

constructor TRESTClient.Create(const BaseURL: string);
begin
  inherited Create;
  FBaseURL := BaseURL;
  FHTTPClient := TFPHTTPClient.Create(nil);
  FHTTPClient.AddHeader('Content-Type', 'application/json');
  FHTTPClient.AddHeader('Accept', 'application/json');
  FHTTPClient.IOTimeout := 10000;
end;

destructor TRESTClient.Destroy;
begin
  FHTTPClient.Free;
  inherited;
end;

function TRESTClient.GetExpenses: TJSONArray;
var
  Response: string;
  Data: TJSONData;
begin
  Response := FHTTPClient.Get(FBaseURL + '/api/expenses');
  Data := GetJSON(Response);
  if Data is TJSONArray then
    Result := Data as TJSONArray
  else
  begin
    Data.Free;
    Result := nil;
  end;
end;

function TRESTClient.CreateExpense(const Desc: string; Amount: Double;
  const Category: string): TJSONObject;
var
  Body: TJSONObject;
  RequestStream, ResponseStream: TStringStream;
  Data: TJSONData;
begin
  Body := TJSONObject.Create;
  try
    Body.Add('description', Desc);
    Body.Add('amount', Amount);
    Body.Add('category', Category);
    Body.Add('date', FormatDateTime('yyyy-mm-dd', Now));

    RequestStream := TStringStream.Create(Body.AsJSON);
    ResponseStream := TStringStream.Create('');
    try
      FHTTPClient.RequestBody := RequestStream;
      FHTTPClient.Post(FBaseURL + '/api/expenses', ResponseStream);

      if FHTTPClient.ResponseStatusCode = 201 then
      begin
        Data := GetJSON(ResponseStream.DataString);
        if Data is TJSONObject then
          Result := Data as TJSONObject
        else
        begin
          Data.Free;
          Result := nil;
        end;
      end
      else
        Result := nil;
    finally
      ResponseStream.Free;
      RequestStream.Free;
    end;
  finally
    Body.Free;
  end;
end;

Parsing a Real-World API Response

Let us consume a real JSON API. Weather services, currency exchanges, and public data APIs all follow similar patterns. Here is a complete example that fetches data from a JSON API and extracts structured information:

program ConsumeAPI;

{$mode objfpc}{$H+}

uses
  SysUtils, Classes, fphttpclient, fpjson, jsonparser;

type
  TExchangeRate = record
    Currency: string;
    Rate: Double;
  end;

function FetchExchangeRates(const BaseCurrency: string): TJSONObject;
var
  Client: TFPHTTPClient;
  Response: string;
  Data: TJSONData;
begin
  Result := nil;
  Client := TFPHTTPClient.Create(nil);
  try
    Client.AddHeader('Accept', 'application/json');
    Client.IOTimeout := 10000;

    try
      { Example API endpoint (fictional) }
      Response := Client.Get(
        'http://api.exchange.example.com/latest?base=' + BaseCurrency);

      if Client.ResponseStatusCode = 200 then
      begin
        Data := GetJSON(Response);
        if Data is TJSONObject then
          Result := Data as TJSONObject
        else
          Data.Free;
      end
      else
        WriteLn('API returned status: ', Client.ResponseStatusCode);
    except
      on E: Exception do
        WriteLn('API error: ', E.Message);
    end;
  finally
    Client.Free;
  end;
end;

procedure DisplayRates(RatesObj: TJSONObject);
var
  I: Integer;
  Key: string;
begin
  WriteLn('Exchange Rates:');
  WriteLn(Format('%-10s %12s', ['Currency', 'Rate']));
  WriteLn(StringOfChar('-', 24));

  for I := 0 to RatesObj.Count - 1 do
  begin
    Key := RatesObj.Names[I];
    WriteLn(Format('%-10s %12.4f', [Key, RatesObj.Items[I].AsFloat]));
  end;
end;

var
  Response: TJSONObject;
  RatesObj: TJSONObject;
begin
  Response := FetchExchangeRates('USD');
  if Response <> nil then
  try
    WriteLn('Base: ', Response.Get('base', 'unknown'));
    WriteLn('Date: ', Response.Get('date', 'unknown'));

    RatesObj := Response.Objects['rates'];
    if RatesObj <> nil then
      DisplayRates(RatesObj);
  finally
    Response.Free;
  end;
end.

This example demonstrates the complete pattern for API consumption: create the HTTP client, set headers and timeouts, make the request, check the status code, parse the JSON response, extract the data you need, and handle errors at every step. This same pattern works for any REST API — weather data, stock prices, social media, payment processing.

The DeleteExpense Method

Completing the REST client from Section 35.5, here is the DELETE operation:

function TRESTClient.DeleteExpense(ID: Integer): Boolean;
var
  Response: TStringStream;
begin
  Result := False;
  Response := TStringStream.Create('');
  try
    try
      FHTTPClient.HTTPMethod('DELETE',
        FBaseURL + '/api/expenses/' + IntToStr(ID),
        Response, []);
      Result := FHTTPClient.ResponseStatusCode in [200, 204];
      if not Result then
        WriteLn('Delete failed: status ', FHTTPClient.ResponseStatusCode);
    except
      on E: Exception do
        WriteLn('Delete error: ', E.Message);
    end;
  finally
    Response.Free;
  end;
end;

Note the use of HTTPMethod('DELETE', ...)TFPHTTPClient does not have a dedicated Delete method, so we use the generic HTTPMethod that accepts any HTTP method as a string. This approach also works for PATCH, HEAD, OPTIONS, and any other HTTP method.

Error Handling for REST Calls

REST APIs return error information in the response body as well as the status code:

function TRESTClient.HandleResponse(StatusCode: Integer;
  const ResponseBody: string): Boolean;
var
  ErrorObj: TJSONData;
  ErrorMsg: string;
begin
  case StatusCode of
    200, 201, 204:
      Result := True;
    400:
    begin
      ErrorObj := GetJSON(ResponseBody);
      try
        ErrorMsg := (ErrorObj as TJSONObject).Get('error', 'Bad request');
        WriteLn('Validation error: ', ErrorMsg);
      finally
        ErrorObj.Free;
      end;
      Result := False;
    end;
    401:
    begin
      WriteLn('Authentication required');
      Result := False;
    end;
    404:
    begin
      WriteLn('Resource not found');
      Result := False;
    end;
    500:
    begin
      WriteLn('Server error — try again later');
      Result := False;
    end;
  else
    WriteLn('Unexpected status: ', StatusCode);
    Result := False;
  end;
end;

35.6 Building MicroServe: A Simple HTTP Server

Now for the centerpiece of this chapter — and one of the most educational exercises in this entire book: building an HTTP server from scratch.

There is a profound difference between using a web framework and understanding how the web works. Millions of developers use Express.js, Django, Rails, or Spring to build web applications, but most could not explain what actually happens between a browser pressing Enter and a page appearing. They know the framework's API, but they do not know what the framework does.

MicroServe closes that gap. It is a minimal but functional HTTP server written entirely in Pascal. It listens for HTTP requests, parses them, routes to handler functions, and sends back responses — HTML, JSON, or plain text.

Building a web server teaches you what web frameworks abstract away. When you understand what happens between a browser pressing Enter and a page appearing, you understand web development at a level that most developers never reach.

The HTTP Protocol

HTTP is a text-based request-response protocol:

Request:

GET /api/expenses HTTP/1.1\r\n
Host: localhost:8080\r\n
Accept: application/json\r\n
\r\n

Response:

HTTP/1.1 200 OK\r\n
Content-Type: application/json\r\n
Content-Length: 42\r\n
\r\n
{"expenses":[],"count":0}

The request has three parts: the request line (method, path, version), headers (key-value pairs), and an optional body. The response has a status line, headers, and a body. Headers and body are separated by a blank line (\r\n\r\n).

MicroServe Architecture

┌─────────────────────────────────────────┐
│ MicroServe                              │
│                                         │
│  Listen on port 8080                    │
│       ↓                                 │
│  Accept connection                      │
│       ↓                                 │
│  Read HTTP request from socket          │
│       ↓                                 │
│  Parse request line (method, path)      │
│       ↓                                 │
│  Parse headers                          │
│       ↓                                 │
│  Route to handler based on path         │
│       ↓                                 │
│  Handler generates response             │
│       ↓                                 │
│  Format HTTP response                   │
│       ↓                                 │
│  Write response to socket               │
│       ↓                                 │
│  Close connection                       │
└─────────────────────────────────────────┘

The Request Parser

type
  THTTPRequest = record
    Method: string;     { GET, POST, PUT, DELETE }
    Path: string;       { /api/expenses }
    Version: string;    { HTTP/1.1 }
    Headers: TStringList;
    Body: string;
  end;

function ParseHTTPRequest(const RawRequest: string): THTTPRequest;
var
  Lines: TStringList;
  Parts: TStringArray;
  I, BlankLine: Integer;
begin
  Result.Headers := TStringList.Create;
  Lines := TStringList.Create;
  try
    Lines.Text := RawRequest;

    if Lines.Count = 0 then Exit;

    { Parse request line: GET /path HTTP/1.1 }
    Parts := Lines[0].Split([' ']);
    if Length(Parts) >= 3 then
    begin
      Result.Method := Parts[0];
      Result.Path := Parts[1];
      Result.Version := Parts[2];
    end;

    { Parse headers }
    BlankLine := -1;
    for I := 1 to Lines.Count - 1 do
    begin
      if Trim(Lines[I]) = '' then
      begin
        BlankLine := I;
        Break;
      end;
      Result.Headers.Add(Lines[I]);
    end;

    { Parse body (everything after blank line) }
    if BlankLine >= 0 then
    begin
      Result.Body := '';
      for I := BlankLine + 1 to Lines.Count - 1 do
      begin
        if Result.Body <> '' then
          Result.Body := Result.Body + #10;
        Result.Body := Result.Body + Lines[I];
      end;
    end;
  finally
    Lines.Free;
  end;
end;

The Response Builder

type
  THTTPResponse = record
    StatusCode: Integer;
    StatusText: string;
    ContentType: string;
    Body: string;
  end;

function FormatHTTPResponse(const Resp: THTTPResponse): string;
begin
  Result := Format('HTTP/1.1 %d %s'#13#10, [Resp.StatusCode, Resp.StatusText]);
  Result := Result + 'Content-Type: ' + Resp.ContentType + #13#10;
  Result := Result + 'Content-Length: ' + IntToStr(Length(Resp.Body)) + #13#10;
  Result := Result + 'Connection: close' + #13#10;
  Result := Result + 'Server: MicroServe/1.0 (FreePascal)' + #13#10;
  Result := Result + #13#10;
  Result := Result + Resp.Body;
end;

function MakeResponse(Code: Integer; const Status, ContentType, Body: string): THTTPResponse;
begin
  Result.StatusCode := Code;
  Result.StatusText := Status;
  Result.ContentType := ContentType;
  Result.Body := Body;
end;

function JSONResponse(Code: Integer; const Body: string): THTTPResponse;
begin
  Result := MakeResponse(Code, 'OK', 'application/json', Body);
  if Code = 404 then Result.StatusText := 'Not Found';
  if Code = 400 then Result.StatusText := 'Bad Request';
  if Code = 500 then Result.StatusText := 'Internal Server Error';
end;

function HTMLResponse(const Body: string): THTTPResponse;
begin
  Result := MakeResponse(200, 'OK', 'text/html; charset=utf-8', Body);
end;

The Router

type
  TRouteHandler = function(const Request: THTTPRequest): THTTPResponse;

  TRoute = record
    Method: string;
    Path: string;
    Handler: TRouteHandler;
  end;

var
  Routes: array of TRoute;

procedure RegisterRoute(const Method, Path: string; Handler: TRouteHandler);
begin
  SetLength(Routes, Length(Routes) + 1);
  Routes[High(Routes)].Method := Method;
  Routes[High(Routes)].Path := Path;
  Routes[High(Routes)].Handler := Handler;
end;

function FindRoute(const Method, Path: string): TRouteHandler;
var
  I: Integer;
begin
  for I := 0 to High(Routes) do
    if (Routes[I].Method = Method) and (Routes[I].Path = Path) then
      Exit(Routes[I].Handler);
  Result := nil;
end;

Putting It Together

With the parser, response builder, and router, MicroServe handles requests in its main loop:

procedure HandleClient(Data: TSocketStream);
var
  Buffer: array[0..8191] of Byte;
  BytesRead: Integer;
  RawRequest: string;
  Request: THTTPRequest;
  Response: THTTPResponse;
  ResponseStr: string;
  Handler: TRouteHandler;
begin
  BytesRead := Data.Read(Buffer, SizeOf(Buffer));
  if BytesRead <= 0 then Exit;

  SetString(RawRequest, PChar(@Buffer[0]), BytesRead);
  Request := ParseHTTPRequest(RawRequest);
  try
    WriteLn(Request.Method, ' ', Request.Path);

    Handler := FindRoute(Request.Method, Request.Path);
    if Handler <> nil then
      Response := Handler(Request)
    else
      Response := JSONResponse(404,
        '{"error":"Not found","path":"' + Request.Path + '"}');

    ResponseStr := FormatHTTPResponse(Response);
    Data.Write(ResponseStr[1], Length(ResponseStr));
  finally
    Request.Headers.Free;
  end;
end;

Registering Application Routes

With the infrastructure in place, we register specific routes for our expense API:

function HandleHome(const Request: THTTPRequest): THTTPResponse;
begin
  Result := HTMLResponse(
    '<html><head><title>MicroServe</title></head>' +
    '<body><h1>Welcome to MicroServe</h1>' +
    '<p>A tiny HTTP server written in Pascal.</p>' +
    '<ul>' +
    '<li><a href="/api/expenses">View Expenses (JSON)</a></li>' +
    '<li><a href="/api/status">Server Status</a></li>' +
    '</ul></body></html>'
  );
end;

function HandleGetExpenses(const Request: THTTPRequest): THTTPResponse;
var
  Arr: TJSONArray;
  Exp: TJSONObject;
begin
  Arr := TJSONArray.Create;
  try
    { In a real server, load from database }
    Exp := TJSONObject.Create;
    Exp.Add('id', 1);
    Exp.Add('description', 'Groceries');
    Exp.Add('amount', 85.50);
    Exp.Add('category', 'food');
    Arr.Add(Exp);

    Exp := TJSONObject.Create;
    Exp.Add('id', 2);
    Exp.Add('description', 'Bus pass');
    Exp.Add('amount', 45.00);
    Exp.Add('category', 'transport');
    Arr.Add(Exp);

    Result := JSONResponse(200, Arr.FormatJSON);
  finally
    Arr.Free;
  end;
end;

function HandleStatus(const Request: THTTPRequest): THTTPResponse;
var
  Status: TJSONObject;
begin
  Status := TJSONObject.Create;
  try
    Status.Add('server', 'MicroServe');
    Status.Add('version', '1.0');
    Status.Add('time', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', Now));
    Status.Add('uptime_seconds', Round((Now - ServerStartTime) * 86400));
    Result := JSONResponse(200, Status.FormatJSON);
  finally
    Status.Free;
  end;
end;

{ Register all routes at startup }
begin
  RegisterRoute('GET', '/', @HandleHome);
  RegisterRoute('GET', '/api/expenses', @HandleGetExpenses);
  RegisterRoute('GET', '/api/status', @HandleStatus);
  RegisterRoute('GET', '/api/health', @HandleHealth);
end;

Serving Static Files

A useful extension to MicroServe is serving static files (HTML, CSS, JavaScript, images) from a directory. This turns MicroServe into a basic web server capable of hosting a dashboard:

function ServeStaticFile(const Request: THTTPRequest): THTTPResponse;
var
  FilePath, Ext, ContentType: string;
  SL: TStringList;
begin
  { Map URL path to filesystem path }
  FilePath := 'public' + Request.Path;

  { Security: prevent path traversal attacks }
  if Pos('..', FilePath) > 0 then
  begin
    Result := JSONResponse(403, '{"error":"Forbidden"}');
    Exit;
  end;

  if not FileExists(FilePath) then
  begin
    Result := JSONResponse(404, '{"error":"File not found"}');
    Exit;
  end;

  { Determine content type from extension }
  Ext := LowerCase(ExtractFileExt(FilePath));
  case Ext of
    '.html', '.htm': ContentType := 'text/html; charset=utf-8';
    '.css':          ContentType := 'text/css';
    '.js':           ContentType := 'application/javascript';
    '.json':         ContentType := 'application/json';
    '.png':          ContentType := 'image/png';
    '.jpg', '.jpeg': ContentType := 'image/jpeg';
    '.svg':          ContentType := 'image/svg+xml';
    '.ico':          ContentType := 'image/x-icon';
  else
    ContentType := 'application/octet-stream';
  end;

  SL := TStringList.Create;
  try
    SL.LoadFromFile(FilePath);
    Result := MakeResponse(200, 'OK', ContentType, SL.Text);
  finally
    SL.Free;
  end;
end;

Note the security check: Pos('..', FilePath) > 0 prevents path traversal attacks where a malicious client requests GET /../../../etc/passwd to read system files. This is a minimal check — a production server would canonicalize the path and verify it stays within the designated public directory.

MicroServe is now a functional HTTP server. Register routes, start listening, and it handles requests. We will extend it with more routes and multi-threading in subsequent chapters.

💡 What MicroServe Teaches Every web framework — Express.js, Django, Rails, Spring — does what MicroServe does, just with more features and more abstraction. By building it yourself, you understand what the framework hides: socket management, HTTP parsing, routing, response formatting, content types. When something goes wrong in production and the framework's error message is unhelpful, this understanding is what lets you diagnose the real problem.


Content Types and MIME

A crucial detail in HTTP responses is the Content-Type header. This tells the client how to interpret the response body. Common content types:

Content-Type Used For
text/html; charset=utf-8 HTML web pages
application/json JSON API responses
text/plain Plain text
text/css CSS stylesheets
application/javascript JavaScript files
image/png PNG images
application/octet-stream Binary data (download)

If MicroServe sends a JSON response with Content-Type: text/html, the browser will try to render the JSON as HTML — displaying it as plain text (if you are lucky) or mangling it (if you are not). Getting the content type right is essential for correct behavior.

The Content-Length header tells the client exactly how many bytes to read. Without it, the client does not know when the response body ends. In HTTP/1.0, the server closes the connection to signal the end; in HTTP/1.1, Content-Length is the primary mechanism (with Transfer-Encoding: chunked as an alternative for streaming responses).

MicroServe Security Considerations

MicroServe is deliberately minimal — it is a teaching tool, not a production server. Real HTTP servers must handle many security concerns that MicroServe ignores:

  • Path traversal: A request for GET /../../../etc/passwd could expose system files if the server naively maps URLs to filesystem paths. MicroServe does not serve files, so this is not an issue here, but any file-serving extension must validate and sanitize paths.

  • Request size limits: A malicious client could send a gigabyte-sized request to consume server memory. Production servers limit request sizes (typically 1-10 MB for bodies, 8 KB for headers).

  • Timeouts: A client that connects but never sends data ("slowloris attack") can tie up server resources. Production servers time out idle connections.

  • HTTPS: All production web traffic should use HTTPS (HTTP over TLS) to prevent eavesdropping and tampering. MicroServe uses plain HTTP; adding TLS requires the OpenSSL library.

These are not reasons to avoid building MicroServe — they are reasons to understand what production servers do that MicroServe does not.

Adding CORS Headers for Browser Access

If you want to access MicroServe's API from a web browser running on a different domain (or even from a local HTML file), you need to add CORS (Cross-Origin Resource Sharing) headers. Without them, the browser's same-origin policy blocks the request:

function AddCORSHeaders(const Resp: THTTPResponse): THTTPResponse;
begin
  Result := Resp;
  { In FormatHTTPResponse, add these headers: }
  { Access-Control-Allow-Origin: * }
  { Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS }
  { Access-Control-Allow-Headers: Content-Type, Accept }
end;

function HandleOptions(const Request: THTTPRequest): THTTPResponse;
begin
  { Respond to preflight OPTIONS requests }
  Result := MakeResponse(204, 'No Content', '', '');
end;

CORS is one of those things that seems pointless until you try to build a web dashboard that talks to your Pascal server — then it becomes the single most frustrating obstacle. Adding the Access-Control-Allow-Origin: * header tells the browser "any domain can access this API." For production use, you would replace * with the specific domain of your web dashboard.

Practical Networking Considerations

Before deploying any networked application, consider these real-world factors:

Firewalls. Your server might be running perfectly on port 8080, but a firewall (Windows Firewall, iptables, corporate firewall) might block incoming connections. When testing, temporarily disable the firewall or add an exception for your port. In production, configure the firewall properly.

Port conflicts. If port 8080 is already in use (another server, a leftover process), your server will fail to bind. Use netstat -an | grep 8080 (Linux) or netstat -an | findstr 8080 (Windows) to check if a port is in use before starting your server.

DNS resolution. When you connect to example.com, the operating system performs a DNS lookup to convert the hostname to an IP address. DNS failures (misconfigured DNS server, no internet) will cause connection failures. The error message is usually "Host not found" or "Name resolution failed."

NAT and private networks. If your server runs on a private network (192.168.x.x, 10.x.x.x), it is not directly accessible from the internet. For external access, you need port forwarding on your router, a public IP address, or a reverse proxy. This is why most modern deployment uses cloud servers with public IPs.

Connection pooling. Creating a new HTTP connection for every request is expensive — it involves a TCP handshake, and possibly a TLS handshake for HTTPS. Production applications reuse connections (HTTP keep-alive) or maintain a pool of connections. TFPHTTPClient supports keep-alive by default in HTTP/1.1 mode.


35.7 WebSocket Basics

HTTP is request-response: the client asks, the server answers, and the connection closes (in HTTP/1.0) or waits (in HTTP/1.1 keep-alive). But what if the server needs to push data to the client without being asked — a chat message, a stock price update, a real-time notification?

WebSockets solve this problem. A WebSocket connection starts as an HTTP request (the "upgrade handshake") and then becomes a persistent, full-duplex connection. Both sides can send messages at any time, without the overhead of HTTP headers on every message.

The WebSocket Handshake

Client → Server:
  GET /ws HTTP/1.1
  Host: localhost:8080
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  Sec-WebSocket-Version: 13

Server → Client:
  HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After this handshake, the connection switches from HTTP to the WebSocket protocol, and both sides can send framed messages.

When to Use WebSockets vs. HTTP

Scenario Use HTTP Use WebSocket
Fetch data on demand Yes No
Push notifications No Yes
Chat application No Yes
REST API Yes No
Real-time dashboard Possible (polling) Better (push)
File upload/download Yes No

Free Pascal does not include a WebSocket library in its standard distribution, but third-party libraries like websocketserver (available via OPM) provide WebSocket support. For most applications, HTTP with periodic polling is simpler and sufficient.


35.8 Project Checkpoint: PennyWise REST Sync

Until this point, PennyWise has been a single-machine application. Your expenses live on your computer, and only on your computer. If your hard drive dies, your data dies with it. If you want to check your spending from your phone, you cannot. If Rosa enters an expense at her office and wants to see it at home, she is out of luck.

REST synchronization changes everything. PennyWise gains the ability to push expenses to a remote server and pull expenses from it. The data lives in two places — local (for speed and offline access) and remote (for backup and multi-device access). This is the same pattern used by email clients (IMAP sync), note-taking apps (Evernote, Notion), password managers (Bitwarden), and countless other applications. It is one of the most important patterns in modern software.

The implementation uses everything we have learned in this chapter: HTTP clients for making requests, JSON for encoding the data, error handling for network failures, and the REST conventions for a clean API.

The Sync Architecture

PennyWise (Desktop)                    Server
    |                                     |
    |  POST /api/expenses                 |
    |  (new expense as JSON)              |
    | ----------------------------------> |
    |                                     | Stores in database
    |  200 OK {"id": 42}                  |
    | <---------------------------------- |
    |                                     |
    |  GET /api/expenses?since=2026-03-20 |
    | ----------------------------------> |
    |                                     | Queries recent expenses
    |  200 OK [...]                       |
    | <---------------------------------- |
    |  Merges into local database         |

The Sync Client

unit FinanceSync;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, FinanceCore;

type
  TSyncClient = class
  private
    FServerURL: string;
    FLastSync: TDateTime;
  public
    constructor Create(const ServerURL: string);
    function PushExpense(const E: TExpense): Boolean;
    function PullExpenses(Since: TDateTime): TExpenseArray;
    function TestConnection: Boolean;
    property LastSync: TDateTime read FLastSync;
  end;

implementation

uses
  fphttpclient, fpjson, jsonparser;

constructor TSyncClient.Create(const ServerURL: string);
begin
  inherited Create;
  FServerURL := ServerURL;
  FLastSync := 0;
end;

function TSyncClient.PushExpense(const E: TExpense): Boolean;
var
  Client: TFPHTTPClient;
  Body: TJSONObject;
  RequestStream, ResponseStream: TStringStream;
begin
  Result := False;
  Client := TFPHTTPClient.Create(nil);
  Body := TJSONObject.Create;
  try
    Body.Add('description', E.Description);
    Body.Add('amount', Double(E.Amount));
    Body.Add('category', CategoryToStr(E.Category));
    Body.Add('date', FormatDateTime('yyyy-mm-dd', E.ExpenseDate));
    Body.Add('recurring', E.IsRecurring);

    RequestStream := TStringStream.Create(Body.AsJSON);
    ResponseStream := TStringStream.Create('');
    try
      Client.AddHeader('Content-Type', 'application/json');
      Client.RequestBody := RequestStream;
      Client.IOTimeout := 10000;

      try
        Client.Post(FServerURL + '/api/expenses', ResponseStream);
        Result := Client.ResponseStatusCode in [200, 201];
        if Result then
          FLastSync := Now;
      except
        on E: Exception do
          WriteLn('Sync error: ', E.Message);
      end;
    finally
      ResponseStream.Free;
      RequestStream.Free;
    end;
  finally
    Body.Free;
    Client.Free;
  end;
end;

function TSyncClient.PullExpenses(Since: TDateTime): TExpenseArray;
var
  Client: TFPHTTPClient;
  Response: string;
  Data: TJSONData;
  Arr: TJSONArray;
  Obj: TJSONObject;
  I: Integer;
begin
  SetLength(Result, 0);
  Client := TFPHTTPClient.Create(nil);
  try
    Client.AddHeader('Accept', 'application/json');
    Client.IOTimeout := 10000;

    try
      Response := Client.Get(FServerURL + '/api/expenses?since=' +
        FormatDateTime('yyyy-mm-dd', Since));

      if Client.ResponseStatusCode = 200 then
      begin
        Data := GetJSON(Response);
        try
          if Data is TJSONArray then
          begin
            Arr := Data as TJSONArray;
            SetLength(Result, Arr.Count);
            for I := 0 to Arr.Count - 1 do
            begin
              Obj := Arr.Objects[I];
              Result[I] := CreateExpense(
                Obj.Get('description', ''),
                Obj.Get('amount', 0.0),
                StrToCategory(Obj.Get('category', 'other')),
                StrToDate(Obj.Get('date', DateToStr(Now)))
              );
            end;
          end;
        finally
          Data.Free;
        end;
        FLastSync := Now;
      end;
    except
      on E: Exception do
        WriteLn('Pull error: ', E.Message);
    end;
  finally
    Client.Free;
  end;
end;

function TSyncClient.TestConnection: Boolean;
var
  Client: TFPHTTPClient;
begin
  Result := False;
  Client := TFPHTTPClient.Create(nil);
  try
    Client.IOTimeout := 5000;
    try
      Client.Get(FServerURL + '/api/health');
      Result := Client.ResponseStatusCode = 200;
    except
      Result := False;
    end;
  finally
    Client.Free;
  end;
end;

end.

What Rosa Experienced

Rosa configures PennyWise to sync with a server Tomás set up. She enters an expense on her desktop — "Groceries, $85.50, Food" — and the sync client pushes it to the server as a POST request with a JSON body. Later, from her phone's browser, she opens the MicroServe dashboard and sees the expense.

"It feels like magic," she says.

"It's not magic," Tomás replies. "It's a POST request with a JSON body hitting a REST endpoint that writes to a database. We could build this in any language. We built it in Pascal."


Conflict Resolution and Sync Strategy

Real-world synchronization must handle a fundamental problem: what happens when the same expense is modified in two places before a sync occurs? Rosa changes an expense's category on her desktop while the web dashboard (edited by Tomás) changes its amount. When the sync runs, which version wins?

There are several strategies:

Last-write-wins: The most recent modification overwrites the other. Simple but can lose data silently.

Server-wins: The server's version is always authoritative. The client overwrites its local copy with the server's data. Safe for read-heavy scenarios.

Client-wins: The client's version overwrites the server. Dangerous in multi-user scenarios.

Merge: The system detects which fields changed on each side and merges non-conflicting changes. If the same field changed on both sides, flag it as a conflict for manual resolution.

PennyWise uses a simplified approach: expenses are append-only (you add new expenses but rarely modify existing ones), so conflicts are rare. New expenses are pushed from client to server; the server assigns the canonical ID. For the rare case where an expense is edited on both sides, server-wins is used.

A full conflict resolution system (like Git's merge or Google Docs' OT/CRDT algorithms) is beyond the scope of this chapter, but understanding the problem is important for any developer building synchronized applications.

Retry Logic and Exponential Backoff

Network operations fail. The server might be temporarily overloaded, a DNS lookup might time out, or the user's Wi-Fi might drop for a few seconds. A robust sync client does not give up on the first failure — it retries with increasing delays:

function RetryableGet(Client: TFPHTTPClient; const URL: string;
  MaxRetries: Integer = 3): string;
var
  Attempt: Integer;
  Delay: Integer;
begin
  Delay := 1000;  { Start with 1 second }
  for Attempt := 1 to MaxRetries do
  begin
    try
      Result := Client.Get(URL);
      if Client.ResponseStatusCode in [200, 201, 204] then
        Exit;  { Success }
      if Client.ResponseStatusCode in [400, 401, 403, 404] then
        Exit;  { Client error — retrying won't help }
    except
      on E: Exception do
      begin
        if Attempt = MaxRetries then
          raise;  { Last attempt — re-raise the exception }
        WriteLn(Format('Attempt %d failed: %s. Retrying in %d ms...',
          [Attempt, E.Message, Delay]));
      end;
    end;
    Sleep(Delay);
    Delay := Delay * 2;  { Exponential backoff: 1s, 2s, 4s }
  end;
end;

This pattern — retry with exponential backoff — is used by virtually every cloud service client library. The increasing delay prevents a flood of retry requests from overwhelming an already-struggling server. AWS, Google Cloud, and Azure all recommend exponential backoff in their API documentation.

Network Error Categories

When handling network errors, it helps to classify them into categories that determine the appropriate response:

Error Category Examples Action
Transient Timeout, DNS failure, 503 Service Unavailable Retry with backoff
Client error 400 Bad Request, 401 Unauthorized, 404 Not Found Fix the request, do not retry
Server error 500 Internal Server Error Retry once, then report
Network down No route to host, connection refused Queue for later, notify user
SSL error Certificate expired, handshake failed Report to user, do not retry

PennyWise's sync client classifies errors and responds accordingly: transient failures are retried silently, client errors are logged and reported, and persistent network-down conditions switch the sync to "offline" mode where changes queue up locally until connectivity returns.


35.9 Summary

This chapter introduced networking and internet programming in Pascal.

TCP/IP fundamentals: The internet runs on a layered protocol stack. TCP provides reliable, ordered communication. Clients initiate connections; servers listen and accept them. Sockets are the programming interface for network communication.

TCP sockets in Free Pascal: The ssockets unit provides TInetSocket (client) and TInetServer (server). Communication uses Read and Write on socket streams. Simple servers are blocking (one client at a time); production servers need threading (Chapter 36).

HTTP clients: The fphttpclient unit provides TFPHTTPClient for making HTTP requests. Get retrieves data; Post sends data. Set headers with AddHeader, handle errors with try-except, and always set timeouts.

REST APIs: REST uses HTTP methods (GET, POST, PUT, DELETE) on URL endpoints, with JSON request and response bodies. A REST client class encapsulates the HTTP calls and JSON parsing.

MicroServe: We built an HTTP server from scratch — parsing HTTP requests, routing to handlers, and formatting HTTP responses. This teaches what web frameworks abstract away: sockets, protocols, and the request-response cycle. MicroServe is functional but single-threaded; Chapter 36 adds concurrent request handling.

WebSockets provide persistent, full-duplex connections for real-time communication. They are appropriate for chat, notifications, and live dashboards, but HTTP is simpler and sufficient for most request-response patterns.

PennyWise gained a FinanceSync unit with a TSyncClient class that pushes expenses to and pulls expenses from a REST API server. Error handling, timeouts, and connection testing make the sync robust against network failures.

Pascal is a compiled, strongly typed language that produces native executables. These qualities make it excellent for network programming: the server is fast (no interpreter overhead), the type system catches errors at compile time (not at 3 AM when the server crashes), and the explicit memory model means resource leaks are visible rather than hidden behind garbage collection. MicroServe — our tiny HTTP server — demonstrates that Pascal is not just a teaching language; it is a systems language capable of building real network infrastructure.