Skip to content

Latest commit

 

History

History
816 lines (590 loc) · 13.8 KB

File metadata and controls

816 lines (590 loc) · 13.8 KB

HTTP Cache

This example shows how to cache dynamic GET responses with Vix middleware.

HTTP cache is useful when a route is safe, repeatable, and expensive enough that you do not want to run the handler every time.

Examples:

GET /api/products
GET /api/categories
GET /api/public-feed
GET /api/search?q=phone
GET /api/users?page=1

This is not static file caching.

Static files are handled by:

app.static_dir(...);

HTTP cache middleware is for dynamic route responses generated by handlers.

What this example builds

The app exposes:

GET /api/health
GET /api/products
GET /api/slow-products
GET /api/products-by-language

The cache is installed on:

/api

Only GET responses are cached.

The example demonstrates:

cache miss
cache hit
cache bypass
query-aware cache keys
Vary headers
slow handler skipped on cache hit

Source

Create a file:

http_cache_demo.cpp

Add this code:

#include <chrono>
#include <string>
#include <thread>
#include <vector>

#include <vix.hpp>
#include <vix/middleware.hpp>
#include <vix/json.hpp>

using namespace vix;

struct Product
{
  int id;
  std::string name;
  double price;
};

static std::vector<Product> products{
  {1, "Laptop", 999.99},
  {2, "Phone", 499.50},
  {3, "Tablet", 299.99}
};

static vix::json::Json product_to_json(const Product &product)
{
  using namespace vix::json;

  return o(
    "id", product.id,
    "name", product.name,
    "price", product.price
  );
}

static vix::json::Json products_to_json()
{
  using namespace vix::json;

  Json items = arr();

  for (const auto &product : products)
  {
    items.push_back(product_to_json(product));
  }

  return items;
}

static void install_middleware(App &app)
{
  app.use("/api", middleware::app::request_id_dev());
  app.use("/api", middleware::app::timing_dev());
  app.use("/api", middleware::app::security_headers_dev());

  app.use("/api", middleware::app::http_cache({
    .ttl_ms = 30'000,
    .allow_bypass = true,
    .bypass_header = "x-vix-cache",
    .bypass_value = "bypass",
    .vary_headers = {"accept-language"}
  }));
}

static void register_routes(App &app)
{
  app.get("/", [](Request &, Response &res)
  {
    res.text("HTTP cache example. Try /api/products or /api/slow-products.");
  });

  app.get("/api/health", [](Request &, Response &res)
  {
    res.json({
      "ok", true,
      "service", "http-cache"
    });
  });

  app.get("/api/products", [](Request &req, Response &res)
  {
    using namespace vix::json;

    const std::string page = req.query_value("page", "1");

    res.json(o(
      "ok", true,
      "source", "origin",
      "page", page,
      "products", products_to_json()
    ));
  });

  app.get("/api/slow-products", [](Request &, Response &res)
  {
    using namespace vix::json;

    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    res.json(o(
      "ok", true,
      "source", "origin",
      "delay_ms", 500,
      "products", products_to_json()
    ));
  });

  app.get("/api/products-by-language", [](Request &req, Response &res)
  {
    using namespace vix::json;

    const std::string language =
      req.has_header("accept-language")
        ? req.header("accept-language")
        : "none";

    res.json(o(
      "ok", true,
      "source", "origin",
      "accept_language", language,
      "products", products_to_json()
    ));
  });
}

int main()
{
  App app;

  install_middleware(app);
  register_routes(app);

  app.run(8080);
  return 0;
}

Run it

vix run http_cache_demo.cpp

The server listens on:

http://127.0.0.1:8080

Test the public route

curl -i http://127.0.0.1:8080/

Expected body:

HTTP cache example. Try /api/products or /api/slow-products.

This route is not under /api, so the HTTP cache middleware does not apply to it.

Test the first cache miss

curl -i http://127.0.0.1:8080/api/products

Expected cache status:

x-vix-cache-status: miss

Expected body shape:

{
  "ok": true,
  "source": "origin",
  "page": "1",
  "products": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 999.99
    }
  ]
}

A miss means:

the cache did not have a valid response
the route handler ran
the response was stored

Test the cache hit

Call the same route again:

curl -i http://127.0.0.1:8080/api/products

Expected cache status:

x-vix-cache-status: hit

A hit means:

the cached response was replayed
the route handler was skipped

This is the main value of HTTP cache middleware.

Test cache bypass

Send the bypass header:

curl -i \
  http://127.0.0.1:8080/api/products \
  -H "x-vix-cache: bypass"

Expected cache status:

x-vix-cache-status: bypass

A bypass means:

the request asked the middleware to skip the cache
the route handler ran

This is useful for debugging and manual refresh.

Test query-aware cache keys

The cache key includes the query string.

First request:

curl -i "http://127.0.0.1:8080/api/products?page=1"

Second request:

curl -i "http://127.0.0.1:8080/api/products?page=1"

Expected second response:

x-vix-cache-status: hit

Different page:

curl -i "http://127.0.0.1:8080/api/products?page=2"

Expected behavior:

different query string
different cache key
new cache miss

The response for page=1 and page=2 should not be shared.

Test the slow route

The route /api/slow-products sleeps for 500 ms before responding.

First request:

time curl -i http://127.0.0.1:8080/api/slow-products

Expected cache status:

x-vix-cache-status: miss

Second request:

time curl -i http://127.0.0.1:8080/api/slow-products

Expected cache status:

x-vix-cache-status: hit

The second request should be faster because the handler is skipped.

Bypass the slow route:

time curl -i \
  http://127.0.0.1:8080/api/slow-products \
  -H "x-vix-cache: bypass"

Expected cache status:

x-vix-cache-status: bypass

The bypassed request runs the slow handler again.

Test Vary headers

The middleware is configured with:

.vary_headers = {"accept-language"}

This means the Accept-Language header becomes part of the cache key.

French request:

curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: fr"

Call it again:

curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: fr"

Expected second response:

x-vix-cache-status: hit

English request:

curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: en"

Expected behavior:

different Accept-Language
different cache key
new cache miss

Use vary_headers when the same path can return different content depending on request headers.

Why Vary matters

Without vary_headers, this route could be wrong:

app.get("/api/products-by-language", [](Request &req, Response &res)
{
  const std::string language =
    req.has_header("accept-language")
      ? req.header("accept-language")
      : "none";

  res.json({
    "accept_language", language
  });
});

If the first cached response was French, an English request could receive the French response.

vary_headers prevents that by separating cache entries.

Good vary headers:

accept-language
accept
x-tenant-id
x-currency

Bad vary headers:

headers that do not change the response
high-cardinality headers with no clear value
random request ids
user agents unless truly needed

A vary header should only be used if it changes the response.

HTTP cache configuration

The example uses:

app.use("/api", middleware::app::http_cache({
  .ttl_ms = 30'000,
  .allow_bypass = true,
  .bypass_header = "x-vix-cache",
  .bypass_value = "bypass",
  .vary_headers = {"accept-language"}
}));

Meaning:

ttl_ms = 30 seconds
allow_bypass = clients can force origin
bypass_header = x-vix-cache
bypass_value = bypass
vary_headers = accept-language participates in the cache key

Use a custom cache instance

You can share a cache instance explicitly.

auto cache = middleware::app::make_default_cache({
  .ttl_ms = 30'000
});

app.use("/api", middleware::app::http_cache_mw({
  .prefix = "/api",
  .only_get = true,
  .ttl_ms = 30'000,
  .allow_bypass = true,
  .bypass_header = "x-vix-cache",
  .bypass_value = "bypass",
  .vary_headers = {"accept-language"},
  .cache = cache,
  .add_debug_header = true,
  .debug_header = "x-vix-cache-status"
}));

Use this when:

multiple middleware instances should share one cache
you want to build the cache object yourself
you want a clearer bootstrap structure

For most examples, middleware::app::http_cache(...) is enough.

Where to install HTTP cache

Install HTTP cache on route groups that contain safe GET routes.

Good:

app.use("/api/products", middleware::app::http_cache({
  .ttl_ms = 30'000
}));

Also good:

app.use("/api/public", middleware::app::http_cache({
  .ttl_ms = 30'000
}));

Be careful with:

app.use("/api", middleware::app::http_cache({
  .ttl_ms = 30'000
}));

This is fine when the middleware only applies to GET, but you should still think about which GET routes under /api are safe to cache.

What should be cached

Good candidates:

public product lists
public categories
public blog posts
public search results
public metadata
slow read-only endpoints

Bad candidates:

current user profile
cart
orders
notifications
private messages
admin dashboard
routes that depend on cookies or hidden session state

The safe rule is:

cache public GET routes first
avoid private user-specific routes until the cache key is designed carefully

Authenticated routes

Be careful with authenticated routes.

If the response depends on the user, the cache key must separate users.

Possible vary inputs:

authorization
x-user-id
x-tenant-id

But those can create many cache entries and can be dangerous if misused.

The recommended starting point is:

do not cache private user-specific routes
cache public GET routes first

HTTP cache vs static file cache

Static file cache:

app.static_dir(
  "public",
  "/",
  "index.html",
  true,
  "public, max-age=3600",
  true,
  true
);

Dynamic HTTP cache:

app.use("/api", middleware::app::http_cache({
  .ttl_ms = 30'000
}));

The difference:

static file cache
  browser/proxy cache policy for files served from public/

dynamic HTTP cache
  server-side response cache for route handlers

Do not use http_cache to explain static files.

Do not use app.static_dir(...) to explain dynamic response caching.

They are different parts of Vix.

HTTP cache vs ETag

HTTP cache and ETag solve different problems.

HTTP cache
  server stores response
  cache hit can skip handler

ETag
  response gets a validation tag
  client can revalidate
  server may return 304

Use HTTP cache when the server should avoid repeated work.

Use ETag when the client should avoid downloading the same body again.

They can be combined, but start with one feature and test the headers.

Common response headers

When HTTP cache is active, you may see:

x-vix-cache-status: miss
x-vix-cache-status: hit
x-vix-cache-status: bypass

When timing middleware is active, you may see:

x-response-time: ...
server-timing: total;dur=...

When request id middleware is active, you may see:

x-request-id: ...

These headers make the example easier to test.

Complete test flow

Run:

vix run http_cache_demo.cpp

First products request:

curl -i http://127.0.0.1:8080/api/products

Second products request:

curl -i http://127.0.0.1:8080/api/products

Bypass:

curl -i \
  http://127.0.0.1:8080/api/products \
  -H "x-vix-cache: bypass"

Query key:

curl -i "http://127.0.0.1:8080/api/products?page=1"
curl -i "http://127.0.0.1:8080/api/products?page=2"

Slow route:

time curl -i http://127.0.0.1:8080/api/slow-products
time curl -i http://127.0.0.1:8080/api/slow-products

Vary header:

curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: fr"

curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: en"

Summary

Use HTTP cache middleware for dynamic GET routes that are safe to replay.

Start with:

app.use("/api", middleware::app::http_cache({
  .ttl_ms = 30'000,
  .allow_bypass = true,
  .bypass_header = "x-vix-cache",
  .bypass_value = "bypass"
}));

Remember:

first request
  miss, handler runs

same request again
  hit, handler skipped

bypass header
  bypass, handler runs

different query string
  different cache key

vary header
  separates responses that depend on request headers

Keep the separation clear:

HTTP cache middleware
  dynamic GET response cache

app.static_dir(...)
  static file serving and static cache headers