Build a Caching Proxy Server in C++

Build a CLI proxy server that caches HTTP responses on disk.

Introduction

30 Second Summary

Every time you hit the same API endpoint twice, the same request travels across the internet and back. A caching proxy short-circuits that round trip by remembering previous responses and serving them locally.

In this project, you will build a command-line caching proxy server in C++ that sits between you and any HTTP API. It forwards requests to an origin server, stores responses on disk, and serves them from cache on repeat visits.

What You'll Build

You'll start your proxy with a single terminal command, point it at any public API, and watch it serve cached responses instantly on the second request.

By the end of this project, you'll have:

  • A running CLI proxy server you can point at any origin URL and make requests through, seeing real API data flow through your proxy to the client.
  • Disk-based response caching with X-Cache: HIT and X-Cache: MISS headers telling you exactly where each response came from.
  • A cache-clearing command that wipes stored responses so you can start fresh and verify the proxy re-fetches from the origin.
  • Secret Mission: TTL-based cache expiration that automatically re-fetches stale entries after a configurable timeout.

Are there any prerequisites?

This project assumes basic C++ knowledge (functions, structs, lambdas) and familiarity with the terminal. You do not need prior experience with networking or HTTP libraries.

Before We Start

Before we write any code, let's lock in what you're about to build and why it matters.

Set Up Your C++ Environment

Building a caching proxy requires two things: a C++ compiler that supports modern language features and an HTTP library to handle network requests.

Without verifying these tools first, you risk cryptic errors later when your code gets complex. In this step, you will confirm your compiler works, download the HTTP library, and prove everything compiles cleanly.

In this step, get ready to:
  • Verify your C++ compiler is installed and working.
  • Download the cpp-httplib header into your project.
  • Compile a test program to confirm the full toolchain.
Verify your C++ compiler

Your Mac ships with Apple Clang as part of the Xcode Command Line Tools. This compiler supports C++17, which your project needs for features like std::filesystem.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal and press Enter to open it.
  • Check if your compiler is installed by running this command:
clang++ --version

Check what appeared in your terminal output.

✔️ I see a version number

You should see output like:

Apple clang version 15.0.0 (clang-1500.1.0.2.4)
Target: arm64-apple-darwin23.2.0
Thread model: posix

Your exact version number will differ, and that is fine. Any Apple Clang version printed here supports C++17.

ⓧ Command not found

You need to install the Xcode Command Line Tools. This package includes the C++ compiler, linker, and standard library headers.

  • Install the command line tools by running:
xcode-select --install

A dialog box will appear asking you to confirm the installation. Click Install and wait for it to finish (this may take a few minutes).

  • After the installation finishes, verify the compiler by running:
clang++ --version

You should now see the Apple Clang version info printed in your terminal.

Create your project directory and download cpp-httplib

Your project uses cpp-httplib, a single-file header-only HTTP library for C++. You download one file and include it. No package manager, no build system, no external dependencies.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir caching-proxy
cd caching-proxy
  • Download the httplib.h header from the v0.52.0 release by running:
curl -O https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.52.0/httplib.h
  • Confirm both the folder and the downloaded file exist by running:
ls

You should see httplib.h listed in the output.

Don't see httplib.h?

Make sure you are inside the caching-proxy directory. Run pwd to check your current path. It should end with /Desktop/caching-proxy.

If the curl command failed, check your internet connection and try running it again. Help me troubleshoot the httplib.h download

Why a single header file?

Most C++ HTTP libraries require complex build systems like CMake or Conan. cpp-httplib ships as a single .h file that you #include directly. This keeps your project self-contained with zero dependency management.

Compile a test program

Before writing any proxy logic, you need to confirm that clang++ can compile and link a C++17 program in this directory. A minimal hello-world program is the fastest way to prove it.

  • Create a main.cpp file in your caching-proxy folder. You can do this by running:
touch main.cpp
  • Open main.cpp in your preferred text editor and paste the following code:
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

What does this code do?

This is the simplest possible C++ program. It includes the standard I/O header and prints a line to the console. If this compiles, your entire toolchain (compiler, linker, standard library) is working.

  • Save main.cpp.
  • Compile it by running this command:
clang++ -std=c++17 -o caching-proxy main.cpp

What does this command do?

  • clang++ invokes the C++ compiler.
  • -std=c++17 tells the compiler to use the C++17 standard, which you need for std::filesystem later.
  • -o caching-proxy names the output binary caching-proxy.
  • main.cpp is the source file to compile.

If the command completes with no output, that means success. Compilers only print something when there is a warning or error.

Before you run the program, what do you think it will print?

  • Run your compiled binary:
./caching-proxy

You should see:

Hello, World!

Your C++17 toolchain is working. The binary you just built will be overwritten in the next step when you add real proxy code.

Seeing a compile error?

Double-check that the code in main.cpp matches the snippet above exactly. Look for missing semicolons or mismatched braces.

If you see permission denied when running ./caching-proxy, the binary may not be executable. Run chmod +x caching-proxy and try again.

Help me fix my compile error

✔️ Awesome, I've got everything!

Great. Double check you have saved main.cpp and that your terminal printed Hello, World! when you ran the binary.

ⓧ I'd like to double check the full code

Here is the complete main.cpp you should have at this point:

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

Your environment is locked in. Next up, you will build a forwarding proxy that receives HTTP requests and relays them to a real API.

Build a Forwarding Proxy

Your C++ toolchain is ready and the httplib.h header is in your project directory. Now it is time to make something happen on the network.

A forwarding proxy sits between a client (you) and an origin server (a remote API). Instead of requesting data directly from the origin, you send your request to the proxy, and the proxy fetches it on your behalf. In this step, you will build that proxy so you can see real API data flowing through your own server.

In this step, get ready to:
  • Parse command-line arguments so the proxy knows which port to listen on and which origin to forward to.
  • Forward incoming HTTP requests to the origin server and return responses to the client.
  • See live API data from dummyjson.com flowing through your proxy in the terminal.
Add CLI argument parsing

Your proxy needs two pieces of information at startup: the port to listen on, and the origin URL to forward requests to. You will read these from command-line flags so the proxy stays flexible.

  • Open main.cpp in your editor and replace the entire file contents with this CLI argument parser:
#include "httplib.h"
#include <iostream>
#include <string>

int main(int argc, char* argv[]) {
    // Parse CLI arguments for port and origin
    int port = 0;
    std::string origin;

    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--port" && i + 1 < argc) {
            port = std::stoi(argv[++i]);
        } else if (arg == "--origin" && i + 1 < argc) {
            origin = argv[++i];
        }
    }

    // Exit with usage message if required args are missing
    if (port == 0 || origin.empty()) {
        std::cerr << "Usage: caching-proxy --port <number> --origin <url>" << std::endl;
        return 1;
    }

    return 0;
}

What does this code do?

The for loop walks through argv looking for --port and --origin flags. When it finds one, it reads the next argument as that flag's value.

If either flag is missing, the program prints a usage message to stderr and exits with code 1. This keeps the CLI self-documenting.

  • Save main.cpp and compile it by running:
clang++ -std=c++17 -o caching-proxy main.cpp
  • Test the usage message by running the binary with no arguments:
./caching-proxy

You should see the usage message printed: Usage: caching-proxy --port <number> --origin <url>.

Seeing a compile error?

If you see 'httplib.h' file not found, make sure httplib.h is in the same directory as main.cpp. Run ls to confirm both files are listed.

help me debug a compile error with httplib.h

Add the forwarding handler

With argument parsing in place, you can now create the actual HTTP server and client. The server catches every incoming request, the client forwards it to the origin, and the response flows back to whoever made the original request.

  • In main.cpp, find the line that says return 0; at the end of the main function.
  • Replace that return 0; with the following forwarding handler code:
    // Create HTTP client pointed at the origin server
    httplib::Client cli(origin);
    cli.set_follow_location(true);

    httplib::Server svr;

    // Intercept all incoming requests before routing
    svr.set_pre_routing_handler(
        [&](const httplib::Request& req, httplib::Response& res) {
            // Forward the request to the origin
            auto result = cli.Get(req.path);
            if (result) {
                std::string content_type = result->get_header_value("Content-Type");
                res.status = result->status;
                res.set_content(result->body, content_type);
                std::cout << "[PROXY] " << req.method << " " << req.path << " -> " << result->status << std::endl;
            } else {
                res.status = 502;
                res.set_content("Bad Gateway: could not reach origin server", "text/plain");
                std::cerr << "[ERROR] " << req.method << " " << req.path << std::endl;
            }
            return httplib::Server::HandlerResponse::Handled;
        });

    std::cout << "Caching proxy server started on port " << port << std::endl;
    std::cout << "Forwarding requests to " << origin << std::endl;

    svr.listen("0.0.0.0", port);

    return 0;

What does this code do?

  • httplib::Client cli(origin) creates an HTTP client pointed at whatever origin URL you pass on the command line.
  • set_follow_location(true) tells the client to automatically follow redirects from the origin.
  • set_pre_routing_handler intercepts every request before the server tries to match it to a route. This is safer than a catch-all regex route, which can overflow on complex paths.
  • The handler forwards the request path to the origin, copies the response status, body, and content type back to the client, and logs the result.
  • If the origin is unreachable, the proxy returns a 502 Bad Gateway error.

Why set_pre_routing_handler instead of a regex route?

The cpp-httplib library warns that using a catch-all regex like svr.Get(".*", handler) can cause a stack overflow during pattern matching on long paths. set_pre_routing_handler sidesteps the regex engine entirely by catching requests before routing even begins.

  • Save main.cpp.
Compile and test the proxy end-to-end

Time to see data flow through your proxy. You will compile the binary, start the proxy pointed at a free public API, and make a request through it.

  • Compile the updated binary by running:
clang++ -std=c++17 -o caching-proxy main.cpp

Compile taking a long time?

The httplib.h header is large (it includes a full HTTP implementation). The first compile can take 10-20 seconds. This is normal.

help me debug a slow or failing compile with httplib.h

  • Start the proxy by running:
./caching-proxy --port 3000 --origin http://dummyjson.com

You should see two lines confirming the server is running:

Caching proxy server started on port 3000
Forwarding requests to http://dummyjson.com

What is dummyjson.com?

It is a free, public REST API that serves fake product, user, and cart data over HTTP. No signup or authentication required. Perfect for testing a proxy.

  • Open a second terminal window. Your proxy is running in the first terminal and will stay running until you stop it.

Before you run the next command, think: what do you expect to see when you send a request to your proxy instead of directly to dummyjson.com?

  • In the second terminal, request data through your proxy by running:
curl http://localhost:3000/products

You should see a JSON response full of product data flowing back. The response starts with something like {"products":[{"id":1,"title":... and contains product objects with titles, prices, and descriptions.

Switch back to your first terminal (where the proxy is running). You should see a log line like:

[PROXY] GET /products -> 200

That confirms your proxy received the request, forwarded it to dummyjson.com, got a 200 response, and passed the data back to you.

Not seeing any output from curl?

  • Make sure the proxy is still running in the first terminal. If it exited, check for compile errors and rebuild.
  • Confirm you are using port 3000 in both the proxy start command and the curl URL.
  • If you see Bad Gateway, check your internet connection. The proxy needs to reach http://dummyjson.com to forward requests.

help me troubleshoot why my caching proxy is not returning data

  • Stop the proxy by pressing Ctrl+C in the first terminal.

✔️ Awesome, I've got everything!

Great. Make sure you have saved main.cpp with all the forwarding logic in place.

ⓧ I'd like to double check the full code

Your main.cpp should contain these sections in order. Scroll up and confirm each chunk is present:

  • The includes (httplib.h, iostream, string) and main function signature with argc and argv.
  • The CLI parsing for loop reading --port and --origin.
  • The usage check that exits if port is 0 or origin is empty.
  • httplib::Client with set_follow_location(true).
  • httplib::Server with set_pre_routing_handler that forwards requests and logs [PROXY] lines.
  • Two cout lines printing the startup message, then svr.listen("0.0.0.0", port).

Real API data is flowing through your own server. Next up, you will add a caching layer so repeated requests skip the network entirely and serve from disk.

Add Response Caching

Your forwarding proxy works. Every request goes through to the origin and comes back with real data.

But what happens when you hit the same endpoint twice? Right now, both requests make a full network round-trip to the origin server, even though the response hasn't changed. A caching layer solves this by storing responses on disk so repeated requests return instantly from local files.

In this step, get ready to:
  • Build cache read/write functions that store HTTP responses as files on disk.
  • Update the proxy handler to serve from cache on repeated requests.
  • Verify caching works with X-Cache headers showing HIT and MISS.
See the problem: every request hits the origin

Before you add caching, observe the gap yourself. Start your proxy and make the same request twice.

  • Start the proxy by running this command:
./caching-proxy --port 3000 --origin http://dummyjson.com
  • Open a second terminal tab and curl the same endpoint twice by running:
curl http://localhost:3000/products/1
curl http://localhost:3000/products/1

Before you look at the proxy's terminal output, predict: did the second request contact the origin again, or did the proxy serve from memory?

Check the proxy terminal. You see two separate [PROXY] log lines. Both requests hit the origin, even though the response was identical. That wasted round-trip is the gap you are about to close.

  • Stop the proxy with Ctrl+C so you can edit main.cpp.
Create the cache data model and write function

You need a way to convert request paths into filenames and serialize HTTP responses to disk. The strategy: hash the request path to create a safe filename, then write status code, content type, and body into that file.

  • In main.cpp, find the line #include <string> at the top. Add the following new includes and cache infrastructure directly below it (above int main):
#include <fstream>
#include <sstream>
#include <filesystem>
#include <functional>

namespace fs = std::filesystem;

const std::string CACHE_DIR = "./proxy_cache";

// Holds a cached HTTP response
struct CachedResponse {
    int status;
    std::string body;
    std::string content_type;
};

// Convert a request path into a safe cache filename
std::string get_cache_path(const std::string& key) {
    size_t hash_val = std::hash<std::string>{}(key);
    return CACHE_DIR + "/" + std::to_string(hash_val) + ".cache";
}

What does this code do?

  • The new includes bring in file I/O (fstream), string streams (sstream), directory operations (filesystem), and hashing (functional).
  • CachedResponse is a struct that holds everything needed to replay a cached response: status code, body, and content type.
  • get_cache_path hashes the request path (like /products/1) into a numeric string, creating a filename safe for any OS.
  • Directly below get_cache_path, add the write_to_cache function:
// Serialize a response to a cache file on disk
void write_to_cache(const std::string& path, const CachedResponse& entry) {
    fs::create_directories(CACHE_DIR);
    std::string cache_file = get_cache_path(path);
    std::ofstream file(cache_file, std::ios::binary);
    std::string header = std::to_string(entry.status) + "\n" +
                         entry.content_type + "\n";
    file.write(header.c_str(), header.size());
    file.write(entry.body.c_str(), entry.body.size());
}

What does write_to_cache do?

  • fs::create_directories(CACHE_DIR) ensures the ./proxy_cache/ folder exists before writing. It creates it automatically on the first cache write.
  • The file stores a simple text header (status code and content type on separate lines) followed by the raw response body in binary mode.
  • Verify these additions compile by running:
clang++ -std=c++17 -o caching-proxy main.cpp

You should see no errors. The new functions are defined but not called yet.

Seeing a compile error?

  • If you see an error about filesystem, make sure you have -std=c++17 in your compile command. The <filesystem> header requires C++17.
  • If you see redefinition of 'CachedResponse', you may have pasted the struct inside main() by mistake. All the new code goes above int main.

Help me fix a compile error in my caching proxy

Add the cache read function and update the handler

Now you need the other half: reading a cached response back from disk. Then you will rewire the proxy handler to check the cache before forwarding to the origin.

  • Below write_to_cache, add the read_from_cache function:
// Load a cached response from disk
bool read_from_cache(const std::string& path, CachedResponse& entry) {
    std::string cache_file = get_cache_path(path);
    std::ifstream file(cache_file, std::ios::binary);
    if (!file.is_open()) return false;

    std::string status_str;
    std::getline(file, status_str);
    entry.status = std::stoi(status_str);
    std::getline(file, entry.content_type);

    std::ostringstream ss;
    ss << file.rdbuf();
    entry.body = ss.str();

    return true;
}

What does read_from_cache do?

  • It opens the cache file for the given path. If the file does not exist, it returns false immediately, signaling a cache miss.
  • It reads the status code and content type from the first two lines, then slurps the rest of the file as the response body using rdbuf().
  • Now find the existing svr.set_pre_routing_handler block inside main(). Replace the entire lambda body (everything between the opening { and closing });) with this cache-check code:
    svr.set_pre_routing_handler(
        [&](const httplib::Request& req, httplib::Response& res) {
            std::string cache_key = req.path;

            // Check cache first
            CachedResponse cached;
            if (read_from_cache(cache_key, cached)) {
                res.status = cached.status;
                res.set_header("X-Cache", "HIT");
                res.set_content(cached.body, cached.content_type);
                std::cout << "[HIT]  " << req.method << " "
                          << req.path << std::endl;
                return httplib::Server::HandlerResponse::Handled;
            }

What does the cache-check section do?

  • cache_key uses the raw request path as the lookup key. Every unique path gets its own cache file.
  • If read_from_cache finds a file for this path, the proxy serves the cached data immediately and attaches an X-Cache: HIT header so you can see it was served from cache.
  • Returning Handled short-circuits the handler. The request never reaches the origin.
  • Directly below the closing } of the if block (still inside the lambda), add the forwarding-with-cache code:
            // Forward to origin on cache miss
            auto result = cli.Get(req.path);
            if (result) {
                std::string content_type =
                    result->get_header_value("Content-Type");
                CachedResponse entry;
                entry.status = result->status;
                entry.body = result->body;
                entry.content_type = content_type;
                write_to_cache(cache_key, entry);
                res.status = result->status;
                res.set_header("X-Cache", "MISS");
                res.set_content(result->body, content_type);
                std::cout << "[MISS] " << req.method << " "
                          << req.path << std::endl;
            } else {
                res.status = 502;
                res.set_content("Bad Gateway: could not reach origin"
                                " server", "text/plain");
                std::cerr << "[ERROR] " << req.method << " "
                          << req.path << std::endl;
            }

            return httplib::Server::HandlerResponse::Handled;
        });

What does the forward-and-cache section do?

  • On a cache miss, the proxy forwards the request to the origin just like before.
  • Before responding to the client, it saves the origin's response into a CachedResponse struct and calls write_to_cache to persist it on disk.
  • The X-Cache: MISS header tells the client this response came fresh from the origin.
Compile and test the caching
  • Save main.cpp and compile the updated proxy by running:
clang++ -std=c++17 -o caching-proxy main.cpp
  • Start the proxy by running:
./caching-proxy --port 3000 --origin http://dummyjson.com
  • In your second terminal tab, predict what you expect to see: will the first request say HIT or MISS? What about the second?
  • Curl the same endpoint twice by running:
curl -i http://localhost:3000/products/1

Look at the response headers. You should see X-Cache: MISS on this first request. The proxy fetched from the origin.

  • Run the same curl command again:
curl -i http://localhost:3000/products/1

This time you should see X-Cache: HIT. The response came straight from disk, no network round-trip to the origin.

  • Inspect the cache directory to confirm the files exist by running:
ls ./proxy_cache/

You should see a .cache file. This is the serialized response for /products/1, named by its hash.

Seeing X-Cache: MISS on both requests?

  • Make sure you did not stop and restart the proxy between the two curl commands. Restarting does not clear cache (it is on disk), but if you recompiled between requests the binary may have changed.
  • Check that ./proxy_cache/ exists after the first request. If it does not, write_to_cache is not being called. Confirm that the write_to_cache(cache_key, entry) line is inside the if (result) block.
  • Verify your read_from_cache is placed above main() so it is visible inside the lambda.

Help me debug my caching proxy - both requests show MISS

✔️ Awesome, I've got everything!

Great. Double check you have saved main.cpp and your proxy is returning X-Cache: HIT on repeated requests.

ⓧ I'd like to double check the full code

Your main.cpp should contain these sections in order. Scroll up and confirm each one is present:

  • Includes: httplib.h, iostream, string, fstream, sstream, filesystem, functional.
  • Namespace alias: namespace fs = std::filesystem;
  • Constant: const std::string CACHE_DIR = "./proxy_cache";
  • Struct: CachedResponse with status, body, content_type fields.
  • Function: get_cache_path hashing the key to a filename.
  • Function: write_to_cache creating the directory and writing status, content type, and body.
  • Function: read_from_cache opening the file, reading status and content type lines, then body.
  • Inside main(): CLI parsing, client creation, then the handler checking cache first (HIT path), forwarding on miss (MISS path), and the error branch (502).

Your proxy now caches every response it fetches. Repeated requests skip the network entirely and serve from local files. Next up, you will add a --clear-cache command so you can purge stale entries without manually deleting files.

Add Cache Clearing

Your proxy caches responses on disk, but what happens when those cached responses go stale? Right now, the only way to clear the cache is to manually delete the ./proxy_cache/ directory.

A proper CLI tool needs a built-in purge command. In this step, you'll add a --clear-cache flag that wipes the cache directory and handles edge cases like missing directories or invalid arguments.

In this step, get ready to:
  • Add a clear_cache() function that removes the cache directory.
  • Update the CLI parser to handle --clear-cache and invalid arguments.
  • Run a full end-to-end test of the complete proxy.
Add the clear_cache function and CLI branch

You need two pieces: a function that does the actual directory removal, and a CLI branch that calls it when the user passes --clear-cache.

  • In main.cpp, add #include <chrono> after your existing includes (you will use this in a later extension).
  • Then add the following clear_cache() function above main(), right after read_from_cache:
void clear_cache() {
    if (fs::exists(CACHE_DIR)) {
        fs::remove_all(CACHE_DIR);
        std::cout << "Cache cleared successfully." << std::endl;
    } else {
        std::cout << "Cache is already empty." << std::endl;
    }
}

What does this code do?

This function checks whether the cache directory exists before trying to remove it.

  • fs::exists(CACHE_DIR) checks if the ./proxy_cache/ directory is on disk.
  • fs::remove_all(CACHE_DIR) recursively deletes the directory and all its contents.
  • The else branch gives the user clear feedback when there is nothing to clear.
  • Now find the argument-parsing for loop inside main(). Locate the --origin branch that ends with:
        } else if (arg == "--origin" && i + 1 < argc) {
            origin = argv[++i];
        }
  • Add a new else if branch for --clear-cache so the closing brace of the --origin block now continues like this:
        } else if (arg == "--origin" && i + 1 < argc) {
            origin = argv[++i];
        } else if (arg == "--clear-cache") {
            clear_cache();
            return 0;
        }

Why return 0 immediately?

The --clear-cache flag is a standalone operation. It does not start the server.

Returning 0 right after calling clear_cache() exits the program cleanly without requiring --port or --origin.

This means the tool supports two distinct modes: proxy mode and cache-management mode.

  • Save main.cpp and compile by running:
clang++ -std=c++17 -o caching-proxy main.cpp
  • Test the new flag by running:
./caching-proxy --clear-cache

If you still have cache files from the previous step, you should see Cache cleared successfully. printed to your terminal.

  • Run it again immediately to confirm the empty-cache path works:
./caching-proxy --clear-cache

This time you should see Cache is already empty. since the directory no longer exists.

Seeing a compile error?

  • Make sure clear_cache() is defined ABOVE main(). C++ requires functions to be declared before they are called.
  • Check that you have #include <filesystem> and the namespace fs = std::filesystem; alias near the top of your file.
  • Help me debug this compile error.
Handle invalid arguments and improve error logging

Right now, if someone runs the binary without --port, --origin, or --clear-cache, the usage message does not mention the cache-clearing option.

  • In main.cpp, find the usage message inside the if (port == 0 || origin.empty()) block:
        std::cerr << "Usage: caching-proxy --port <number> --origin <url>" << std::endl;
        return 1;
  • Replace that block with an updated version that shows both usage modes:
        std::cerr << "Usage: caching-proxy --port <number> --origin <url>"
                  << std::endl;
        std::cerr << "       caching-proxy --clear-cache" << std::endl;
        return 1;
  • Next, scroll down to the else block inside the pre-routing handler where the proxy returns a 502 error. Find this line:
                std::cerr << "[ERROR] " << req.method << " " << req.path << std::endl;
  • Replace it with improved error logging that includes the actual error reason:
                std::cerr << "[ERROR] " << req.method << " " << req.path
                          << " - " << httplib::to_string(result.error())
                          << std::endl;

What does this improve?

httplib::to_string(result.error()) converts the connection error code into a human-readable string. This makes debugging network issues much easier than a bare [ERROR] line with no explanation of what went wrong.

  • Save main.cpp and compile again:
clang++ -std=c++17 -o caching-proxy main.cpp
  • Test with no arguments to verify the updated usage message:
./caching-proxy

You should see both usage lines printed:

Usage: caching-proxy --port <number> --origin <url>
       caching-proxy --clear-cache

Not seeing the second usage line?

  • Make sure the new std::cerr line is BEFORE return 1; inside the if (port == 0 || origin.empty()) block.
  • Confirm you recompiled after saving. The old binary will not reflect your edits.
  • Help me fix my usage message.
Run a full end-to-end test

Your caching proxy now supports two modes: server mode and cache-clearing mode. Time to verify the complete lifecycle works from start to finish.

  • Start the proxy by running:
./caching-proxy --port 3000 --origin http://dummyjson.com

You should see the startup messages confirming the proxy is listening on port 3000.

  • Open a second terminal tab or window. Make a request through the proxy:
curl -i http://localhost:3000/products

Look at the response headers. You should see X-Cache: MISS because this is the first time this path has been requested.

  • Run the exact same curl command again:
curl -i http://localhost:3000/products

This time you should see X-Cache: HIT. The response came from your local cache instead of the origin server.

  • Stop the proxy with Ctrl+C in the first terminal.
  • Clear the cache by running:
./caching-proxy --clear-cache

You should see Cache cleared successfully. printed.

  • Confirm the cache directory is gone by running:
ls ./proxy_cache

You should see an error like No such file or directory. The cache is completely gone.

  • Now restart the proxy:
./caching-proxy --port 3000 --origin http://dummyjson.com

Before you curl the same endpoint again, what do you expect X-Cache to say this time?

  • In your second terminal, run:
curl -i http://localhost:3000/products

You should see X-Cache: MISS. The cache was cleared, so the proxy had to fetch from the origin again. Your caching proxy is feature-complete.

  • Stop the proxy with Ctrl+C.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.cpp and that your binary is compiled with the latest changes.

ⓧ I'd like to double check the full code

Your main.cpp should contain these sections in order. Scroll through your file and confirm each one is present:

  • Includes: httplib.h, iostream, string, fstream, sstream, filesystem, functional, chrono.
  • The namespace fs alias and CACHE_DIR constant.
  • The CachedResponse struct.
  • Functions: get_cache_path, write_to_cache, read_from_cache, clear_cache.
  • The main() function with --port, --origin, and --clear-cache parsing.
  • The updated usage message showing both modes.
  • The pre-routing handler with HIT/MISS logic and improved error logging.

Your caching proxy is now a complete CLI tool with two modes of operation. Next up, you will tackle a bonus challenge that makes the cache even smarter.

Secret mission

Add TTL-Based Cache Expiration

Real caches do not store responses forever. What if you could set a time limit on cached entries so they automatically expire and get re-fetched from the origin? In this secret mission, you will add a --ttl flag that makes your proxy production-aware.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Your project directory on the Desktop containing main.cpp, httplib.h, and the compiled caching-proxy binary.
  • The ./proxy_cache/ directory with cached response files (negligible disk space).

Keep everything running

No action needed. Choose this if you plan to keep developing your caching proxy or want to reference the code later.

  • Nothing is running in the background once you stop the proxy with Ctrl+C.
  • The cache files in ./proxy_cache/ take negligible disk space.
  • No cloud resources or subscriptions were created.

Pause - I'll come back to this later

Clear the cached responses to free up disk space while keeping your source code intact.

  • Clear the cache by running:
./caching-proxy --clear-cache
  • Your main.cpp and httplib.h files remain on disk so you can recompile and run the proxy again anytime.

Delete - I don't want to use this again

Remove all project files and cached data from your machine entirely.

  • First, clear the cache by running:
./caching-proxy --clear-cache
  • Then move up to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf caching-proxy
  • On Windows, use this command instead:
cd ~\Desktop
Remove-Item -Recurse -Force caching-proxy
  • Confirm the folder is gone by running ls ~/Desktop and verifying the project folder no longer appears.

Nice Work!

Nice Work!

You just built a fully functional caching proxy server in C++ from a single source file. Your tool sits between a client and any HTTP API, forwarding requests, caching responses to disk, and serving repeat requests instantly.

You've learned how to:

  • Build a CLI caching proxy server that accepts --port and --origin flags and forwards HTTP requests to any origin using cpp-httplib's server and client APIs.
  • Implement disk-based response caching with custom X-Cache: HIT and X-Cache: MISS headers that tell you exactly where each response came from.
  • Add a standalone --clear-cache command that purges cached responses from disk, giving you full control over cache invalidation from the command line.
  • Secret Mission: Added TTL-based automatic cache expiration so stale entries are re-fetched from the origin after a configurable timeout.

Ready to quiz yourself?