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.

POST, PATCH and DELETE with JSON through a Connection key
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 as ETag and X-RateLimit-Remaining are 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-Cache works 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_supported rather than sending your body to a new host.
  • Cookies never travel and upstream Set-Cookie headers are stripped. Use credentials: 'omit': with credentials: 'include' the browser blocks the response, because the proxy never sends Access-Control-Allow-Credentials.

Free vs Pro for write requests

cors.dev limits for POST, PUT, PATCH and DELETE
LimitAnonymous, freePro, $5 per month
MethodsGET and HEADGET, HEAD, POST, PUT, PATCH and DELETE
A POST from the browser405 method_not_allowedForwarded with its body
Request bodyNone; 400 if sentUp to 1 MiB, any content type
Authorization and custom headersRefused with 400Forwarded
Response size1 MiB, text types6 MiB, any content type
Upstream deadline10 seconds10 seconds
Preflight OPTIONSAnswered by the proxyAnswered by the proxy, not counted
CachingNoneGET only, X-Cors-Cache 1 to 300 seconds
Rate600 requests per minute per IP, shared pool600 requests per minute, 10 concurrent
Monthly requestsNo quota500,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 no X-Cors-Key or 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 in Retry-After. 429 quota_exceeded: the billing period is used up; add 500,000 requests for $5.
  • 502 response_too_large or redirect_not_supported, 504 upstream_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.