Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Ahondev/portfolio-v2/llms.txt

Use this file to discover all available pages before exploring further.

Every REST endpoint registered under /api/v1/ is automatically protected by a lightweight rate-limiting layer provided by RateLimitServiceProvider. Because the framework is designed to run behind Cloudflare, the rate limiter is written to extract the real client IP from Cloudflare’s forwarded headers before it builds the per-IP counter key — preventing the 429 responses from being triggered by a single proxy address shared across thousands of real users.

How Rate Limiting Works

RateLimitServiceProvider::boot() hooks into WordPress’s rest_pre_dispatch filter (priority 10). This filter runs before the REST router dispatches to any controller, making it the earliest possible interception point for every API request.
add_filter('rest_pre_dispatch', function ($result, $server, $request) {

    // 1. Extract the real client IP (Cloudflare-aware)
    $ip = $_SERVER['HTTP_CF_CONNECTING_IP']
        ?? $_SERVER['HTTP_X_FORWARDED_FOR']
        ?? $_SERVER['REMOTE_ADDR'];

    // 2. Build a unique key per IP + route
    $key   = 'rate_' . md5($ip . $request->get_route());

    // 3. Read the current request count
    $count = get_transient($key) ?: 0;

    // 4. Enforce the limit
    if ($count > 100) {
        return [
            'success' => false,
            'code'    => 429,
            'error'   => [
                'message' => 'Too many requests',
                'context' => 'global',
            ],
        ];
    }

    // 5. Increment and persist for 60 seconds
    set_transient($key, $count + 1, 60);

    return $result;

}, 10, 3);

IP Resolution Order

The IP is resolved by falling through three headers in order of preference:
1

HTTP_CF_CONNECTING_IP

Cloudflare sets this header to the original visitor’s IP on every proxied request. It is the most reliable source when Cloudflare is in front of your server.
2

HTTP_X_FORWARDED_FOR

Standard forwarding header used by most load-balancers and reverse proxies. Used as a fallback when CF-Connecting-IP is absent (e.g. direct traffic, other CDNs).
3

REMOTE_ADDR

The TCP connection’s remote address. Always available as the last-resort fallback, but may be a proxy address in production environments.

Rate Limit Parameters

ParameterValue
Max requests100 per window
Window duration60 seconds
Key granularityPer IP address + route (not global)
Storage backendWordPress transients
HTTP status on limit429 Too Many Requests
Because the key is scoped to ip + route, a client that exceeds the limit on POST /api/v1/ssg/all is not blocked from accessing GET /api/v1/contact — each route has its own independent counter.

429 Response Shape

When the limit is exceeded the filter short-circuits the REST dispatch and returns this structure directly:
{
  "success": false,
  "code": 429,
  "error": {
    "message": "Too many requests",
    "context": "global"
  }
}
Because rest_pre_dispatch returns a value instead of null, WordPress skips controller dispatch entirely and sends this response immediately. No controller code runs after a 429.

Customising the Limit

The rate limiter is implemented entirely inside RateLimitServiceProvider::boot(). To change the threshold or window, extend the provider and override boot():
// app/Providers/CustomRateLimitServiceProvider.php

namespace Ahon\WPCMS\App\Providers;

class CustomRateLimitServiceProvider extends RateLimitServiceProvider
{
    public function boot(): void
    {
        add_filter('rest_pre_dispatch', function ($result, $server, $request) {

            $ip  = $_SERVER['HTTP_CF_CONNECTING_IP']
                ?? $_SERVER['HTTP_X_FORWARDED_FOR']
                ?? $_SERVER['REMOTE_ADDR'];

            $key   = 'rate_' . md5($ip . $request->get_route());
            $count = get_transient($key) ?: 0;

            // Custom: 50 requests per 30 seconds
            if ($count > 50) {
                return [
                    'success' => false,
                    'code'    => 429,
                    'error'   => [
                        'message' => 'Too many requests',
                        'context' => 'global',
                    ],
                ];
            }

            set_transient($key, $count + 1, 30);

            return $result;

        }, 10, 3);
    }
}
Then register your provider in the Kernel instead of the default one.
You can also conditionally skip rate limiting for certain routes by inspecting $request->get_route() inside the filter and returning $result unchanged for internal or admin-only endpoints.
Rate limiting uses WordPress transients as its storage backend. Flushing the cache (via the admin bar button or any other transient-clearing operation) will reset all rate-limit windows immediately, potentially allowing a burst of requests from previously limited IPs. See Caching for details on the cache flush behaviour.

Caching

Rate-limit counters live alongside other transients — learn how the flush button affects both systems.

SSG REST API

The SSG endpoints (/api/v1/ssg/*) are subject to the same rate-limiting rules as all other REST routes.

Build docs developers (and LLMs) love