CORSProxy
A small PHP CORS proxy intended for browser-side code that needs to reach servers without CORS headers.
composer require wp-php-toolkit/corsproxy
A Playground-style browser tool reads https://api.github.com/repos/WordPress/php-toolkit, a plugin ZIP from downloads.wordpress.org, or a raw fixture from GitHub. The browser blocks the response when the upstream server does not send the required CORS headers, even though PHP can fetch the same public URL server-side.
The CORSProxy component is that server-side bridge. It accepts a target URL, fetches it from PHP, and returns a browser-readable response. Because an open proxy is a security and abuse risk, real deployments should add host allowlists, rate limits, header controls, and private-network protections appropriate to their environment.
Run the proxy locally
Run on your machine: the proxy needs to listen on a port. Start PHP's built-in server and request any HTTPS URL through it.
PLAYGROUND_CORS_PROXY_DISABLE_RATE_LIMIT=1 \
php -S 127.0.0.1:5263 vendor/wp-php-toolkit/corsproxy/cors-proxy.php
# In another terminal:
curl -s "http://127.0.0.1:5263/cors-proxy.php/https://api.github.com/repos/WordPress/php-toolkit" | head
Production rate limiting
Drop a cors-proxy-config.php next to cors-proxy.php. If that file defines a playground_cors_proxy_maybe_rate_limit() function, the proxy calls it before forwarding any request — your one chance to reject early. Without that function the proxy relies only on its coarse built-in safeguards, so production deployments should provide their own rate limiting.
This example uses a per-IP token bucket stored on disk. Replace with Redis or memcached for multi-host deployments.
Allowlist upstream hosts
Out of the box the proxy will fetch any public URL. Most real deployments want a fixed list of upstreams — GitHub, Packagist, wp.org. Both the rate-limit logic and the allowlist live in the same hook, since cors-proxy.php only calls playground_cors_proxy_maybe_rate_limit() once. The example below shows just the allowlist concern; in practice you stack both in one function inside cors-proxy-config.php.
Browser-side fetch through the proxy
Once deployed, the client side is just fetch() with the proxy URL. Drop this into any HTML page.
const PROXY = "https://cors.example.com/cors-proxy.php";
async function viaProxy(url, init = {}) {
const res = await fetch(`${PROXY}/${url}`, {
...init,
headers: {
...(init.headers || {}),
"X-Cors-Proxy-Allowed-Request-Headers": "Authorization",
},
});
if (!res.ok) throw new Error(`Proxy returned ${res.status}`);
return res;
}
const repo = await viaProxy("https://api.github.com/repos/WordPress/php-toolkit").then(r => r.json());
console.log(repo.full_name, repo.stargazers_count);
Deploy behind nginx
The proxy is a single PHP script — any SAPI works. nginx + php-fpm is a common production setup. PATH_INFO is what the proxy reads to learn the target URL.
server {
listen 443 ssl http2;
server_name cors.example.com;
root /var/www/cors-proxy;
index cors-proxy.php;
location ~ ^/cors-proxy\.php(/.*)?$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_param SCRIPT_FILENAME $document_root/cors-proxy.php;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
}
}