GuidesJSON BODIES
CORS proxy for POST, PUT, PATCH and DELETE
cors.dev Pro sends JSON bodies up to 1 MiB from browser code: $5 per month for 500,000 requests, 7-day trial, no card. The trial includes 1,000 requests. Custom headers and Authorization are forwarded, responses can be up to 6 MiB, and the proxy answers preflights itself without counting them. The free anonymous proxy is GET and HEAD only and answers a POST with 405.
Why a cross-origin POST fails
A POST with Content-Type: application/json, an Authorization header or any custom header, and every PUT, PATCH and DELETE, makes the browser send an OPTIONS preflight first. The API has to answer it with Access-Control-Allow-Methods and Access-Control-Allow-Headers that cover your request, then send Access-Control-Allow-Origin on the real response. Most third-party APIs do neither, so the request fails in the browser while it works in curl or Postman. CORS preflight failed walks through the error messages.
The anonymous cors.dev proxy relays GET and HEAD only: a POST gets 405 with X-Cors-Error: method_not_allowed and Allow: GET, HEAD, OPTIONS, and a GET with a body gets 400. Writes go through a trial or Pro Connection.
Send a JSON POST, PATCH or DELETE
Create a Connection in the dashboard with your app origin, such as https://app.example.com or http://localhost:5173, and the API host, such as api.example.com. Then call the proxy with any method and send the Connection key in X-Cors-Key. Method, body and headers stay your normal fetch call.
const PROXY = 'https://proxy.cors.dev/';
const headers = {
'X-Cors-Key': 'YOUR_CONNECTION_KEY',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
};
const created = await fetch(PROXY + 'https://api.example.com/v1/items', {
method: 'POST',
headers,
body: JSON.stringify({ name: 'New item' }),
credentials: 'omit',
});
const item = await created.json(); // a 201 from the API arrives as 201
await fetch(PROXY + `https://api.example.com/v1/items/${item.id}`, {
method: 'PATCH', // capitals: fetch does not uppercase PATCH for you
headers,
body: JSON.stringify({ name: 'Renamed' }),
credentials: 'omit',
});
const removed = await fetch(PROXY + `https://api.example.com/v1/items/${item.id}`, {
method: 'DELETE',
headers,
credentials: 'omit',
});
if (!removed.ok) {
throw new Error(`HTTP ${removed.status}: ${removed.headers.get('X-Cors-Error') ?? 'upstream error'}`);
}What happens to the preflight
The proxy answers every OPTIONS preflight itself with a 204: Access-Control-Allow-Methods: GET, HEAD, POST, PUT, PATCH, DELETE, the request headers you asked for echoed back, your exact origin in Access-Control-Allow-Origin and Access-Control-Max-Age: 600, so the browser reuses that answer for 10 minutes. The API never sees the OPTIONS request, and preflights do not count toward your monthly requests.
Browsers never put custom headers on a preflight, so the key, origin and host checks run on the real request that follows. A preflight can pass while that request fails with 401 invalid_key, 403 origin_not_allowed or 403 target_not_allowed: read the X-Cors-Error header on the second request.
Bodies, responses and status codes
- Request bodies up to 1 MiB in any format: JSON, URL-encoded or multipart forms, plain text or binary. A larger body gets 413
request_too_large. - Responses of any content type up to 6 MiB, with the API status unchanged: a 201, 204, 409 or 422 reaches your code as it was sent, marked
X-Cors-Source: upstream. Response headers such asETagandX-RateLimit-Remainingare readable from JavaScript. - The API gets 10 seconds to answer. After that the proxy returns 504
upstream_timeout, and the write may or may not have happened on the API side. - Writes are sent once, never retried and never cached.
X-Cors-Cacheworks on GET only, and a POST that sends it gets 400. - Redirects are followed for up to 4 hops when every hop host is on your Connection. A redirect to another host that would resend the body fails with 502
redirect_not_supportedrather than sending your body to a new host. - Cookies never travel and upstream
Set-Cookieheaders are stripped. Usecredentials: 'omit': withcredentials: 'include'the browser blocks the response, because the proxy never sendsAccess-Control-Allow-Credentials.
Free vs Pro for write requests
| Limit | Anonymous, free | Pro, $5 per month |
|---|---|---|
| Methods | GET and HEAD | GET, HEAD, POST, PUT, PATCH and DELETE |
| A POST from the browser | 405 method_not_allowed | Forwarded with its body |
| Request body | None; 400 if sent | Up to 1 MiB, any content type |
| Authorization and custom headers | Refused with 400 | Forwarded |
| Response size | 1 MiB, text types | 6 MiB, any content type |
| Upstream deadline | 10 seconds | 10 seconds |
| Preflight OPTIONS | Answered by the proxy | Answered by the proxy, not counted |
| Caching | None | GET only, X-Cors-Cache 1 to 300 seconds |
| Rate | 600 requests per minute per IP, shared pool | 600 requests per minute, 10 concurrent |
| Monthly requests | No quota | 500,000 per billing period, $5 per extra 500,000 |
| Price | $0, no signup | $5 per month; 7-day trial with 1,000 requests, no card |
Errors you can hit on a write
- 405
method_not_allowed: the request had noX-Cors-Keyor the account has no active trial or Pro, so the GET-only rules applied, or the preflight asked for a lowercase method. - 401
invalid_key: the key is malformed, unknown or revoked. - 403
origin_not_allowed: the page origin is not on the Connection. Compare scheme, host and port. - 403
target_not_allowed: the API host, or a redirect hop, is not on the Connection. - 413
request_too_large: the body is over 1 MiB. - 429
rate_limited: wait the seconds inRetry-After. 429quota_exceeded: the billing period is used up; add 500,000 requests for $5. - 502
response_too_largeorredirect_not_supported, 504upstream_timeout: the API sent more than 6 MiB, redirected the body elsewhere or took longer than 10 seconds.
Good questions.
Does the free cors.dev proxy support POST?
No. Anonymous requests are GET and HEAD only, and a POST gets 405 with X-Cors-Error: method_not_allowed. POST, PUT, PATCH and DELETE need a Connection key on the 7-day trial or on Pro at $5 per month.
Why does my PATCH fail when POST works?
fetch uppercases GET, HEAD, POST, PUT, DELETE and OPTIONS for you, but not PATCH. A lowercase 'patch' goes out as is, it matches nothing in the allowed methods, and the proxy answers the preflight with 405. Write the method as 'PATCH'.
Can I upload files?
Yes, up to 1 MiB per request in total. Build a FormData object and leave Content-Type unset, so the browser adds the multipart boundary itself. Larger uploads need to go to the API directly or through your own server.
Are POST requests retried if the API times out?
No. Each write is sent once. After 10 seconds you get 504 upstream_timeout without knowing whether the API applied it, so send an Idempotency-Key header when the API supports one; Pro forwards it like any custom header.
Do preflight requests count toward the 500,000?
No. The proxy answers OPTIONS itself without contacting the API, and only the real requests that follow are counted.
Can I send writes to localhost or a private IP?
No. Destinations must be public HTTPS hosts on port 443. localhost works as the origin your app runs on, for example http://localhost:5173, but not as the API host.