V2: Imitate a correct OPTIONS request

1. My Caddy version (caddy -version):


2. How I run Caddy:

I’m trying to send requests directly from a web browser to an external server that I don’t have control of.
I’m using Caddy as a reverse proxy so I can manipulate CORS headers and allow browser to correctly handle the response.

a. System environment:

Windows 10

b. Command:

.\caddy run

d. My complete Caddyfile:

:5002 {
    reverse_proxy /jsonrpc <public_IP>:443 {
        header_up Host <public_IP>
        header_up -X-Forwarded-For
        header_down Access-Control-Allow-Headers "Content-Type"
        header_down Access-Control-Allow-Origin "*"
        header_down Access-Control-Allow-Methods "GET, POST, OPTIONS"
        transport http {

Where <public_ip> is the url of my server.

3. The problem I’m having:

From JavaScript I’m sending a POST request through a standard fetch() function.

fetch(url, {
        cache: 'no-store',
        headers: {
          'Content-Type': 'application/json',
        body: JSON.stringify(request),
        .then((resp) => {
          if (!resp.ok) {
            throw Error(resp.statusText);
          return resp.json();
        .then((resp) => {
        .catch((error) => {
          console.error('Error:', error);

Web browser preflights it by a OPTIONS request and it seems like my backend server does not understand it and sends 400 Bad request back.
So I think I need a way to imitate a valid response from the server to a OPTIONS request so the web browser will proceed further. But I don’t know how.

4. Error messages and/or full log output:

Browser console:


Request (from the web browser tools):

Host: localhost:5002
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: */*
Accept-Language: en-US,en;q=0.7,pl;q=0.3
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: http://localhost:8080/
Origin: http://localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache


HTTP/1.1 400 Bad Request
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Origin: *
Content-Length: 274
Content-Type: text/html
Date: Mon, 17 Feb 2020 11:11:56 GMT
Server: Caddy
Server: Werkzeug/0.9.6 Python/2.7.9
Via: 1.1 <public_ip>

5. What I already tried:

To backup my claims that it is backend server refusing to handle OPTIONS request I’ve send a POST request with a Postman and it is working just fine.
While OPTIONS always returns some kind of 400 Bad request. For example it says it is expecting only json type and when I change content type to json it says the body can not be empty and so on. So it looks like the backend server isn’t configured for OPTIONS.

Howdy @Gumaa,

Faking a valid OPTIONS response is definitely one of the weirder requests I’ve seen, but I’ve definitely also seen pickier backends before, so…

A valid response is described by Mozilla on their developer web docs - both for a regular OPTIONS request and for a CORS preflight OPTIONS request. For a CORS preflight:

The server responds with Access-Control-Allow-Methods and says that POST , GET , and OPTIONS are viable methods to query the resource in question. This header is similar to the Allow response header, but used strictly within the context of CORS.

HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT 
Server: Apache/2.0.61 (Unix) 
Access-Control-Allow-Origin: http://foo.example 
Access-Control-Allow-Methods: POST, GET, OPTIONS 
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type 
Access-Control-Max-Age: 86400 
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100 
Connection: Keep-Alive


So we need:

  • An empty 204 No Content response
  • The usual CORS headers you’d put on
  • Plus Access-Control-Allow-Methods to respond to the OPTIONS request

It looks like you’re already applying all those headers when you reverse proxy. However, you’re going to need to use advanced matchers to ensure that OPTIONS requests get a preconfigured responder while normal requests get proxied.

Have a look at this advanced matcher example in particular:

To match on anything other than a path, define a named matcher and refer to it using @name :

@post {
	method POST
reverse_proxy @post localhost:9000

Caddyfile Concepts — Caddy Documentation

Seems almost perfect for us… Just need to tweak it to look for OPTIONS instead of POST, add in the path and instead of using it on the reverse proxy, we want to use it for the responder and for the headers.

The respond directive is what you’re after for the Status 204, and the header directive for the CORS headers.

Now, just to keep things neat, I’d recommend you use a handle directive to bundle the respond and header directives together in a neat package and tell Caddy to exclusively use that for any OPTIONS request.

So to sum up:

  1. Define your OPTIONS + /jsonrpc matcher
  2. handle that matcher specifically
  3. Throw a respond and a header directive in that handle

As a quick addendum, since the CORS preflight is a different kind of OPTIONS request to a regular one, and we probably don’t wanna respond to a regular OPTIONS request with a CORS preflight response, you can also differentiate the two easily by the presence of the Access-Control-Request-Method request header.

This means if you want to be extra neat about it, you can also specify the presence of this header in your advanced matcher so the handle only catches the CORS preflight version and the regular version will be… well, it’ll get 400’d, but that would make more sense than a CORS response anyway and you only need the CORS to work here.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.