Contents
Introduction
This tutorial explains, in complete detail, how to implement a server-side proxy endpoint in WordPress to access external APIs from browser-based clients and avoid CORS issues. It covers why a proxy is useful, security and performance considerations, concrete plugin code you can drop into your WordPress site, caching and rate-limiting strategies, how to forward headers and request bodies safely, and how to use the proxy from client-side code.
Why a proxy endpoint and how it solves CORS
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that blocks web pages from making requests to a different origin unless the target explicitly allows it. Many third-party APIs either do not set permissive CORS headers or restrict origins. By creating a proxy endpoint on your WordPress server, the browser requests are made to the same origin as your site (or to an origin you control), avoiding the browsers cross-origin checks. The WordPress server then performs the external API request and returns the result to the browser.
Benefits
- Eliminates browser CORS issues for APIs that dont expose permissive headers
- Allows you to centralize API keys and secrets on the server (never expose them to the client)
- Enables request/response normalization, caching, rate limiting, and analytics
Trade-offs / caveats
- Increased server bandwidth and latency (your server relays data)
- Potential security risks if the proxy is open to arbitrary external hosts (avoid open proxy)
- Need for careful input validation, rate-limiting, and error handling
Design goals for a safe and flexible WordPress proxy
- Use the WordPress REST API to register a custom endpoint
- Restrict proxied targets via a whitelist or allowlist
- Support common HTTP methods (GET, POST, PUT, DELETE) as needed
- Pass through or sanitize selected headers and query parameters
- Store API keys on the server and inject them into proxied requests as required
- Implement caching and rate-limiting
- Return meaningful HTTP status codes and content-types
- Set CORS response headers on the WordPress endpoint when the client origin differs from WordPress
Implementation: WordPress plugin example
Below is a complete plugin example you can place in wp-content/plugins/. It registers a REST route, validates requests, forwards them to an allowed external API, caches GET responses for a configurable time, and applies simple rate-limiting per IP. Customize the allowlist, timeouts, and caching to your needs.
WP_REST_Server::ALLMETHODS, callback => wpapi_proxy_handle_request, permission_callback => wpapi_proxy_permission_callback, ) ) } / Permission callback. - You can allow public access, or restrict to authenticated users. - Here we allow public access but implement server-side protections below. / function wpapi_proxy_permission_callback( request ) { return true // modify if you require authentication } / Main proxy handler. / function wpapi_proxy_handle_request( WP_REST_Request request ) { // Configuration allowed_hosts = array( api.example.com, another-api.example, // include only trusted hosts ) cache_ttl_seconds = 60 // cache GET responses for 60s rate_limit_max = 60 // max requests per IP per window rate_limit_window = 60 // window size in seconds // 1) Extract incoming params method = request->get_method() query_params = request->get_query_params() body = request->get_body() headers = request->get_headers() // 2) Required param: target url (full URL or path) // Prefer a single url param you may also compute based on endpoint mapping target = isset( query_params[url] ) ? query_params[url] : null if ( ! target ) { return new WP_REST_Response( array( error => Missing url parameter ), 400 ) } // 3) Normalize and validate target host parsed = wp_parse_url( target ) if ( empty( parsed[host] ) ) { return new WP_REST_Response( array( error => Invalid url parameter ), 400 ) } host = parsed[host] if ( ! in_array( host, allowed_hosts, true ) ) { return new WP_REST_Response( array( error => Target host not allowed ), 403 ) } // 4) Rate limiting by IP (simple transient-based) ip = wp_get_client_ip ? wp_get_client_ip() : ( isset(_SERVER[REMOTE_ADDR]) ? _SERVER[REMOTE_ADDR] : unknown ) rate_key = wpapi_proxy_rate_ . md5( ip ) rate_data = get_transient( rate_key ) if ( ! rate_data ) { rate_data = array( count => 0, start => time() ) } if ( rate_data[start] rate_limit_window < time() ) { rate_data = array( count => 0, start => time() ) } rate_data[count] set_transient( rate_key, rate_data, rate_limit_window ) if ( rate_data[count] > rate_limit_max ) { return new WP_REST_Response( array( error => Rate limit exceeded ), 429 ) } // 5) Cache handling for GET cache_key = wpapi_proxy_cache_ . md5( method . . target . . serialize( query_params ) . . body ) if ( GET === strtoupper( method ) ) { cached = get_transient( cache_key ) if ( cached ) { // Return cached response with headers response = new WP_REST_Response( cached[body], 200 ) foreach ( cached[headers] as hk => hv ) { response->header( hk, hv ) } // Allow client origins if needed (configure accordingly) response->header( Access-Control-Allow-Origin, ) return response } } // 6) Prepare remote request arguments args = array( method => method, headers => array(), timeout => 15, redirection => 5, blocking => true, ) // Forward only safe headers or specific ones you need allowed_forward_headers = array( accept, content-type, authorization ) foreach ( headers as name => value ) { name_l = strtolower( name ) if ( in_array( name_l, allowed_forward_headers, true ) ) { args[headers][ name ] = value[0] } } // Attach body for non-GET methods if ( GET !== strtoupper( method ) ) { // If content-type is JSON, pass body as-is content_type = isset( args[headers][content-type] ) ? args[headers][content-type] : if ( stripos( content_type, application/json ) !== false ) { args[body] = body } else { // Fallback to parsed body args[body] = request->get_body_params() } } // 7) Inject server-side API keys if needed // Example: if target host requires an API key in a header if ( api.example.com === host ) { api_key = get_option( wpapi_proxy_api_key_example ) // store in WP option or constant if ( api_key ) { args[headers][X-API-KEY] = api_key } } // 8) Perform the remote request remote = wp_remote_request( target, args ) if ( is_wp_error( remote ) ) { error_message = remote->get_error_message() return new WP_REST_Response( array( error => Remote request failed, details => error_message ), 502 ) } code = wp_remote_retrieve_response_code( remote ) remote_body = wp_remote_retrieve_body( remote ) remote_headers = wp_remote_retrieve_headers( remote ) // 9) Optionally cache GET responses if ( GET === strtoupper( method ) code >= 200 code < 300 ) { cache_payload = array( body => remote_body, headers => remote_headers, ) set_transient( cache_key, cache_payload, cache_ttl_seconds ) } // 10) Prepare response to client response = new WP_REST_Response( remote_body, code ) // Transfer some headers like content-type if ( isset( remote_headers[content-type] ) ) { response->header( Content-Type, remote_headers[content-type] ) } elseif ( isset( remote_headers[Content-Type] ) ) { response->header( Content-Type, remote_headers[Content-Type] ) } else { response->header( Content-Type, application/json charset=utf-8 ) } // Security: Do not forward backend-set cookies to the browser unless intended // Add CORS header to allow your frontend origin (adjust to your domain) response->header( Access-Control-Allow-Origin, ) response->header( Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS ) response->header( Access-Control-Allow-Headers, Authorization, Content-Type ) return response }
Notes about the plugin example
- allowed_hosts: Replace the hostnames with only the trusted external APIs you need. Never allow arbitrary hosts — that creates an open proxy.
- API keys: Store API keys in a secure place (wp-config.php constants, or WP options and ensure proper capability controls). The sample uses get_option() for illustration. Do not embed secrets in client-side code.
- Caching: Uses WordPress transients. For larger payloads or production, use an external cache like Redis or object caching. Adjust TTLs as appropriate.
- Rate limiting: The example uses a simple transient per IP. For robust rate limiting, use a more advanced system (e.g., Redis counters or integration with WAF/CDN).
- Timeouts: Adjust wp_remote_request timeout to your requirements to avoid blocking long requests.
- Response headers: The plugin sets Access-Control-Allow-Origin: for simplicity. In production, set a specific origin or implement dynamic origin checking to avoid broad exposure.
Client-side usage examples
Below are example client-side fetch requests demonstrating how to call the proxy endpoint. Replace the endpoint URL to match your WordPress site path, e.g. /wp-json/wpapi-proxy/v1/proxy?url=https://api.example.com/endpoint
Simple GET using fetch
// Example: GET a proxied resource const proxiedUrl = /wp-json/wpapi-proxy/v1/proxy?url= encodeURIComponent(https://api.example.com/data?limit=10) fetch(proxiedUrl, { method: GET, headers: { Accept: application/json } }) .then(response =gt { if (!response.ok) throw new Error(Network response was not ok: response.status) return response.json() }) .then(data =gt { console.log(Proxied data:, data) }) .catch(err =gt { console.error(Fetch failed:, err) })
POST with JSON body
const target = encodeURIComponent(https://api.example.com/create) const proxiedUrl = /wp-json/wpapi-proxy/v1/proxy?url= target fetch(proxiedUrl, { method: POST, headers: { Accept: application/json, Content-Type: application/json }, body: JSON.stringify({ name: Alice, email: alice@example.com }) }) .then(resp =gt resp.json()) .then(data =gt console.log(Created:, data)) .catch(err =gt console.error(err))
Handling CORS for different frontend origins
If your frontend is served from a different origin than your WordPress site (for example, a SPA hosted on a CDN or a different domain), the browser will still enforce CORS when calling the WordPress proxy endpoint. To allow the browser to call the proxy endpoint from other origins, the WordPress proxy must set appropriate Access-Control-Allow-Origin headers. Prefer returning a specific origin rather than , and support credentials only when necessary.
Dynamic origin example (concept)
Validate the Origin request header and echo it back only if it matches an allowlist. In the PHP code above, you can read the Origin header via request->get_header(origin) and decide whether to include it in Access-Control-Allow-Origin. Do not echo back arbitrary origins.
Security best practices
- Whitelist external hosts: Permit only known, trusted API hosts. Never accept arbitrary target URLs.
- Protect secrets: Keep API keys and secrets on the server. Inject them into proxied requests server-side.
- Rate-limit: Implement rate-limiting to prevent abuse and accidental overload of your server or the backend API.
- Validate inputs: Sanitize and validate query params, paths, and request bodies before using them in remote calls.
- Do not forward cookies indiscriminately: Be careful about forwarding Set-Cookie or allowing proxied responses to set cookies for clients.
- Log and monitor: Log proxied requests and failures for debugging and to detect abuse patterns.
- Limit allowed methods: If only GET is needed, allow only GET to reduce risk surface area.
- HTTP header hygiene: Forward only the headers you explicitly need and omit the rest (avoid forwarding X-Forwarded headers unless intentional).
Advanced topics
1) Authentication and user-scoped requests
If you need user-specific proxied calls (e.g., user’s OAuth tokens), do not accept tokens from the client in plain form. Use secured server-side storage of refresh tokens or perform OAuth flows server-side and associate tokens to authenticated WordPress users. Validate that the current WP user has permission to access the proxied resource.
2) Streaming large responses
For large binary responses (images, files), use wp_remote_request in streaming mode or serve files via a server-level proxy to avoid memory exhaustion. Ensure appropriate response headers (Content-Type, Content-Length) are preserved. Consider using X-Accel-Redirect (nginx) or X-Sendfile for efficient serving if you store files temporarily.
3) Using a reverse proxy or CDN
If your WordPress instance is behind a CDN or reverse proxy, you can configure the CDN to proxy the external API directly, reducing load on WordPress. However, implementing the logic inside WordPress gives you full control over authentication and aggregation.
4) Using server-side caching layers
For high-throughput scenarios, place a caching layer in front of your proxy (Varnish, Fastly, Cloudflare) and set caching headers from WordPress. Use cache keys that account for query params and authentication state.
Troubleshooting common issues
- 401/403 from external API: The external API requires authentication — ensure you send the correct server-side credentials.
- 502/504 from the proxy: The external API timed out or is unreachable. Increase timeout or check the remote host health.
- Large responses causing memory issues: Use streaming, avoid loading large payloads into memory, or configure server to handle large bodies.
- Client still sees CORS errors: Confirm the WordPress proxy response includes proper Access-Control-Allow-Origin header and that the browser request includes expected headers. Also verify the request is actually going to your WordPress domain and not directly to the third-party host.
- Open proxy risks: Test that only allowed hosts can be requested. Try various host inputs and ensure the allowlist blocks everything else.
Quick checklist before going to production
- Whitelist external hosts and restrict allowed HTTP methods
- Ensure API keys are stored securely (do not hard-code in plugin file)
- Implement robust caching strategy for GETs
- Enforce rate-limiting and monitoring
- Set specific Access-Control-Allow-Origin values (avoid wildcard in production)
- Test with various error codes and edge cases
- Document which client applications are allowed to call the proxy
Additional useful snippets
Example: Validate and echo specific Origin header (PHP)
// inside your handler, to set dynamic CORS allowed_origins = array( https://app.example.com, https://admin.example.com, ) origin = isset(_SERVER[HTTP_ORIGIN]) ? _SERVER[HTTP_ORIGIN] : if ( in_array( origin, allowed_origins, true ) ) { response->header( Access-Control-Allow-Origin, origin ) response->header( Vary, Origin ) } else { // do not set Access-Control-Allow-Origin (browser will block cross-origin) }
Example: Storing an API key in wp-config.php
// In wp-config.php define( EXAMPLE_API_KEY, your_secret_key_here ) // In plugin api_key = defined(EXAMPLE_API_KEY) ? EXAMPLE_API_KEY :
Resources
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |