HTTP client and web server

Objeck's standard library has an HTTP/1.1 client and server (Web.HTTP and Web.HTTP.Server, in the net and net_server libraries), an HTTP/2 client and an HTTP/3 (QUIC) client. This page covers the HTTP/1.1 pair: how to answer requests, how to make them, and what v2026.10.3 changed about connections.

Compile with:

obc -src program.obs -lib net,net_server,json,cipher

(Leave out net_server if you only make requests.)

The two shipped examples

Read those first. The program below adds a POST route and counts connections.

A server, a client and a connection count

A server is a class that inherits from HttpRequestHandler and fills in ProcessGet and ProcessPost. Each receives a Request (path, query parameters, headers, body) and a Response to fill in; returning false says the request is fully answered. WebServer->Serve(class, address, port) runs it and does not return until the server stops, so the program starts it on a thread, with port 0 meaning "any free port", and asks WebServer->GetBoundPort() which port it got.

The server makes one handler object for each connection it accepts, and that handler serves every request that arrives on the connection. So a counter incremented in the handler's constructor counts connections, which is what lets this program show whether the client reuses them.

use System.Concurrency;
use System.IO.Filesystem;
use Web.HTTP.Server;
use Data.JSON;

class CountingHandler from HttpRequestHandler {
  @connections : static : Int;
  @requests : static : Int;
  @lock : static : ThreadMutex;

  New() {
    Parent();
    critical(@lock) {
      @connections += 1;
    };
  }

  method : ProcessGet(request : Request, response : Response) ~ Bool {
    critical(@lock) {
      @requests += 1;
    };

    # /hello?name=Ada answers {"hello":"Ada"}
    if(request->GetPath()->Equals("/hello")) {
      name := request->GetParam("name");
      if(name = Nil) {
        name := "world";
      };
      response->SetCodeTypeContent(200, "application/json", "{\"hello\":\"{$name}\"}");
    }
    else {
      response->SetCodeContent(404, "Not found");
    };

    return false;
  }

  # POST /total with a JSON array of numbers answers their sum
  method : ProcessPost(request : Request, response : Response) ~ Bool {
    critical(@lock) {
      @requests += 1;
    };

    parser := JsonParser->New(request->GetContent());
    if(<>parser->Parse()) {
      response->SetCodeContent(400, "Not JSON");
      return false;
    };

    sum := 0.0;
    numbers := parser->GetRoot();
    each(i : numbers->Size()) {
      sum += numbers->Get(i)->GetFloat();
    };
    response->SetCodeTypeContent(200, "application/json", "{\"total\":{$sum}}");
    return false;
  }

  function : Prepare() ~ Nil {
    @lock := ThreadMutex->New("counting_handler");
  }

  function : GetConnections() ~ Int {
    return @connections;
  }

  function : GetRequests() ~ Int {
    return @requests;
  }
}

class HttpKeepAlive {
  function : Main(args : String[]) ~ Nil {
    CountingHandler->Prepare();

    # Serve() blocks, so it runs on its own thread; port 0 asks for any free port
    server := Thread->New() {
      New() {
        Parent();
      }

      method : public : Run(arg : System.Base) ~ Nil {
        WebServer->Serve(CountingHandler->New()->GetClass(), "127.0.0.1", 0);
      }
    };
    server->Execute(Nil);

    waited := 0;
    while(WebServer->GetBoundPort() = 0 & waited < 10000) {
      Thread->Sleep(25);
      waited += 25;
    };
    port := WebServer->GetBoundPort();
    if(port = 0) {
      "The server did not start."->PrintLine();
      return;
    };
    # the handler made to name the class counts as a connection; start from there
    base := CountingHandler->GetConnections();

    # --- twenty GETs from one client ---
    client := Web.HTTP.HttpClient->New();
    ok := 0;
    each(i : 20) {
      url := Web.HTTP.Url->New("http://127.0.0.1:{$port}/hello?name=n{$i}");
      response := client->Get(url);
      if(response <> Nil & response->GetCode() = 200) {
        ok += 1;
      };
    };
    "GET:  {$ok} of 20 answered, {$(CountingHandler->GetConnections() - base)} connection(s) used"->PrintLine();

    # one body, to see what the server sends
    response := client->Get(Web.HTTP.Url->New("http://127.0.0.1:{$port}/hello?name=Ada"));
    "      {$(response->GetContent()->ToString())}"->PrintLine();

    # --- three POSTs: each one opens its own connection ---
    before := CountingHandler->GetConnections();
    body := "[1.5, 2.5, 3]"->ToByteArray();
    each(i : 3) {
      response := client->Post(Web.HTTP.Url->New("http://127.0.0.1:{$port}/total"), body, "application/json");
      if(i = 0) {
        "POST: {$(response->GetContent()->ToString())}"->PrintLine();
      };
    };
    "POST: 3 sent, {$(CountingHandler->GetConnections() - before)} new connection(s) used"->PrintLine();

    # --- close what is still waiting, then stop the server ---
    Web.HTTP.HttpClient->CloseIdleConnections();
    WebServer->Stop();
    "requests served: {$(CountingHandler->GetRequests())}"->PrintLine();
  }
}
obc -src http_keepalive.obs -lib net,net_server,json,cipher
obr http_keepalive.obe
GET:  20 of 20 answered, 1 connection(s) used
      {"hello":"Ada"}
POST: {"total":7.000000}
POST: 3 sent, 3 new connection(s) used
requests served: 24

Twenty GETs from one HttpClient used one connection; three POSTs used three. Run with a toolchain built before this change, the same program (without the CloseIdleConnections line) printed 20 connection(s) used for the GETs.

Connection reuse in v2026.10.3

Before 10.3 every request opened a connection and closed it. The side that closes first keeps the local port in TIME_WAIT for about two minutes, and on Windows a process making about 130 requests a second used all 16,384 ports, after which every connect failed at once (issues #1443, #1503 and #1516).

Now HttpClient keeps plain-HTTP connections:

If your program stops one server and starts another on the same port within four seconds, and relied on every request getting a fresh connection, wait for the old server to exit first.

Where next