The existing REST API snippets on the site only cover disabling or locking down the API entirely. This covers a completely different need – rate limiting your own custom endpoints per IP using transients, without a plugin.
/**
* Rate-limit a custom REST API endpoint.
*
* Demonstrates the pattern on a /wp-json/wpfolks/v1/contact endpoint.
* Apply wpfolks_rest_rate_limit() to any custom route's permission_callback.
*
* Defaults: 10 requests per 60 seconds per IP.
* Override via constants in wp-config.php:
* define( 'WPFOLKS_REST_RATE_LIMIT', 10 );
* define( 'WPFOLKS_REST_RATE_WINDOW', 60 );
*/
// ─── Configuration Defaults ──────────────────────────────────────────────────
defined( 'WPFOLKS_REST_RATE_LIMIT' ) || define( 'WPFOLKS_REST_RATE_LIMIT', 10 );
defined( 'WPFOLKS_REST_RATE_WINDOW' ) || define( 'WPFOLKS_REST_RATE_WINDOW', 60 );
// ─── Register a rate-limited custom endpoint ──────────────────────────────────
add_action( 'rest_api_init', 'wpfolks_register_contact_endpoint' );
function wpfolks_register_contact_endpoint(): void {
register_rest_route( 'wpfolks/v1', '/contact', [
'methods' => WP_REST_Server::CREATABLE, // POST only
'callback' => 'wpfolks_handle_contact_request',
'permission_callback' => 'wpfolks_check_contact_permissions', // Security & Rate limit combined
'args' => [
'email' => [
'required' => true,
'sanitize_callback' => 'sanitize_email',
'validate_callback' => fn( $v ) => is_email( $v ),
],
'message' => [
'required' => true,
'sanitize_callback' => 'sanitize_textarea_field',
],
],
] );
}
// ─── Permissions & Security Router ───────────────────────────────────────────
function wpfolks_check_contact_permissions( WP_REST_Request $request ): true|WP_Error {
// Verify CSRF Nonce for logged-in users to prevent cross-site request forgery
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( is_user_logged_in() && ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error(
'rest_forbidden',
__( 'Invalid security token.', 'textdomain' ),
[ 'status' => 403 ]
);
}
// Run the rate limit check
return wpfolks_rest_rate_limit();
}
// ─── Rate limit logic ─────────────────────────
function wpfolks_rest_rate_limit(): true|WP_Error {
$ip = wpfolks_login_get_ip(); // Reuse the safe IP resolver from snippet #1
$key = 'wpfolks_rest_rl_' . md5( $ip );
$hits = (int) get_transient( $key );
if ( $hits >= WPFOLKS_REST_RATE_LIMIT ) {
return new WP_Error(
'rate_limited',
__( 'Too many requests. Please slow down.', 'textdomain' ),
[ 'status' => 429 ]
);
}
// Update hits count and reset window expiration cleanly
set_transient( $key, $hits + 1, WPFOLKS_REST_RATE_WINDOW );
return true;
}
// ─── Safe IP Resolver ─────────────────────────────────────────────────────────
function wpfolks_login_get_ip(): string {
// If you use Cloudflare, look for their official header first
if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
$cf_ip = trim( $_SERVER['HTTP_CF_CONNECTING_IP'] );
if ( filter_var( $cf_ip, FILTER_VALIDATE_IP ) ) {
return $cf_ip;
}
}
// Direct connection fallback (safest default)
$remote_ip = $_SERVER['REMOTE_ADDR'] ?? '';
if ( filter_var( $remote_ip, FILTER_VALIDATE_IP ) ) {
return $remote_ip;
}
return '127.0.0.1';
}
// ─── Endpoint handler ──────────────────────────────────────────────
function wpfolks_handle_contact_request( WP_REST_Request $request ): WP_REST_Response {
$email = $request->get_param( 'email' );
$message = $request->get_param( 'message' );
// Handle your contact logic here — e.g. wp_mail() to admin
wp_mail(
get_option( 'admin_email' ),
__( 'New Contact Form Submission', 'textdomain' ),
sprintf( "From: %snn%s", $email, $message )
);
return new WP_REST_Response(
[ 'success' => true, 'message' => __( 'Message received. Thank you!', 'textdomain' ) ],
200
);
}