V2: need help with the json configuration for custom 404 and redirects

1. My Caddy version (caddy version):

2.0 beta 14

2. How I run Caddy:

caddy run --config /etc/caddy/config.json

a. System environment:

CentOS 8, fresh droplet from DigitalOcean

d. My complete Caddyfile or JSON config:

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "match": [
                {
                  "host": [
                    "sorry, cannot make this public"
                  ]
                }
              ],
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "encodings": {
                            "gzip": {},
                            "zstd": {}
                          },
                          "handler": "encode"
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "rewrite",
                                  "strip_path_prefix": "/frontend/"
                                }
                              ]
                            },
                            {
                              "handle": [
                                {
                                  "handler": "file_server",
                                  "hide": [],
                                  "root": "/var/www/html/frontend"
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/frontend*"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "rewrite",
                                  "strip_path_prefix": "/api/"
                                }
                              ]
                            },
                            {
                              "handle": [
                                {
                                  "handler": "reverse_proxy",
                                  "headers": {
                                    "request": {
                                      "set": {
                                        "Host": [
                                          "{http.request.host}"
                                        ],
                                        "X-Forwarded-For": [
                                          "{http.request.remote.host}"
                                        ],
                                        "X-Forwarded-Proto": [
                                          "{http.request.scheme}"
                                        ],
                                        "X-Real-Ip": [
                                          "{http.request.remote.host}"
                                        ]
                                      }
                                    }
                                  },
                                  "health_checks": {
                                    "active": {
                                      "expect_status": 2,
                                      "path": "/health"
                                    }
                                  },
                                  "transport": {
                                    "protocol": "http",
                                    "read_buffer_size": 4096
                                  },
                                  "upstreams": [
                                    {
                                      "dial": "10.132.66.221:1325"
                                    }
                                  ]
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/api*"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "rewrite",
                                  "uri": "{http.matchers.file.relative}"
                                }
                              ],
                              "match": [
                                {
                                  "file": {
                                    "try_files": [
                                      "{http.request.uri}",
                                      "404.html",
                                      "/404.html"
                                    ]
                                  }
                                }
                              ]
                            },
                            {
                              "handle": [
                                {
                                  "handler": "file_server",
                                  "hide": [],
                                  "root": "/var/www/html/site"
                                }
                              ]
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "terminal": true
            }
          ]
        }
      }
    }
  },
  "logging": {
    "sink": {
      "writer": {
        "output": "file",
        "filename": "/var/log/caddy/std.log",
        "roll": true,
        "roll_size_mb": 2,
        "roll_gzip": false,
        "roll_local_time": false,
        "roll_keep": 5,
        "roll_keep_days": 10
      }
    },
    "logs": {
      "default": {
        "writer": {
          "output": "file",
          "filename": "/var/log/caddy/caddy.log",
          "roll": true,
          "roll_size_mb": 2,
          "roll_gzip": false,
          "roll_local_time": false,
          "roll_keep": 5,
          "roll_keep_days": 10
        }
      }
    }
  }
}

3. The problem I’m having:

With the above configuration, i am trying to display a custom 404 page and it does not work.
I am sure it’s something easy but i just can’t find it. This is my first interaction with caddy 2.

I also need to find a way to redirect some old pages, for example, everything that comes to domain.com/{terms,privacy-policy,cookie-policy}/ must go to domain.com/legal/{terms,privacy-policy,cookie-policy}. I tried to do this with the rewrite module, but to no avail.
How does one do a 301 redirect in caddy 2, for multiple resources?

I tried above to map some requests that hit /frontend to another location on the server, which is different from / and which will use a file server to serve the contents, does that look correct?

While i did try to enable logging, for some reason, the 404 errors don’t get logged, any idea why?
I also noticed JSON Config Structure - Caddy Documentation but there’s no example anywhere on how this would work, any example would help a lot.

Thanks.

To do redirects, you’ll want to use the static_response handler.

Example:

{
  "handler": "static_response",
  "headers": {"Location":["/somewhere"]},
  "status_code": 302
}

I think you would set your Location header to something like /legal{http.request.uri.path}.

Thank you!
I am making progress with this, apparently this will work:

{
  "handle": [
    {
      "handler": "static_response",
      "headers": {
        "Location": ["/legal{http.request.uri}"]
      },
      "status_code": 301,
      "close": true
    }
  ],
  "match": [
    {
      "path_regexp": {"name": "legal", "pattern": "^(\/)(terms|(cookie|privacy)-policy)(\/)?"}
    }
  ]
},

And it will also carry the query string with it, so i guess this is the way to do this?

How about the custom 404 pages, shouldn’t that be pretty straight forward to accomplish?

Here’s an example from someone else who implemented a matcher on HTTP status codes: caddy_verbose_auth_fail_config1.json5 · GitHub

Unfortunately, right now, I think you’ll need to set a vars with the status code to make it available to the matcher.

There’s an open issue for planning how to make this process simpler with matchers that can compare values: v2: proposal: Matcher to compare primitive values · Issue #3051 · caddyserver/caddy · GitHub

Hope that helps!

Thanks, that helps.
Looking at this error route:

{
  "match": [
    {
      "vars": {
        "status_code": "404"
      }
    }
  ],
  "handle": [
    {
      "handler": "static_response",
      "status_code": "{http.error.status_code}"
    },
    {
      "handler": "rewrite",
      "uri": "/404.html"
    }
  ],
  "terminal": true
},

I’d say it should work, at least the logic should be to match any 404 and rewrite it to the /404.html page, but unfortunately it’s not working. Unfortunately this seems more complex than it should be.

Did you add this part?

              "handle": [{
                "handler": "vars",
                "status_code": "{http.error.status_code}"
              }]

That’s what makes status_code available to use in the matcher:

              "match": [{
                "vars": { "status_code": "401" }
              }],

Like @francislavoie pointed out, this will be easier, soon: v2: proposal: Matcher to compare primitive values · Issue #3051 · caddyserver/caddy · GitHub – feel free to join in that discussion!

Yes, of course :smiley:

Sure, i get it.

So if it’s still not working, please give us more information to go off of. What’s your full config? What behaviour are you seeing exactly? Anything useful in the logs?

1 Like

Sorry, i the morning i was in a bit of a rush, here’s my current config:

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "match": [
                {
                  "host": [
                    "...."
                  ]
                }
              ],
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "encodings": {
                            "gzip": {},
                            "zstd": {}
                          },
                          "handler": "encode"
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "rewrite",
                                  "strip_path_prefix": "/frontend/"
                                }
                              ]
                            },
                            {
                              "handle": [
                                {
                                  "handler": "file_server",
                                  "hide": [],
                                  "root": "/var/www/html/frontend"
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/frontend*"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "rewrite",
                                  "strip_path_prefix": "/api/"
                                }
                              ]
                            },
                            {
                              "handle": [
                                {
                                  "handler": "reverse_proxy",
                                  "headers": {
                                    "request": {
                                      "set": {
                                        "Host": [
                                          "{http.request.host}"
                                        ],
                                        "X-Forwarded-For": [
                                          "{http.request.remote.host}"
                                        ],
                                        "X-Forwarded-Proto": [
                                          "{http.request.scheme}"
                                        ],
                                        "X-Real-Ip": [
                                          "{http.request.remote.host}"
                                        ]
                                      }
                                    }
                                  },
                                  "health_checks": {
                                    "active": {
                                      "expect_status": 2,
                                      "path": "/health-check"
                                    }
                                  },
                                  "transport": {
                                    "protocol": "http",
                                    "read_buffer_size": 4096
                                  },
                                  "upstreams": [
                                    {
                                      "dial": "10.132.66.221:1325"
                                    }
                                  ]
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/api*"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "static_response",
                          "headers": {
                            "Location": [
                              "/legal{http.request.uri}"
                            ]
                          },
                          "status_code": 301,
                          "close": true
                        }
                      ],
                      "match": [
                        {
                          "path_regexp": {
                            "name": "legal",
                            "pattern": "^(/)(terms|(cookie|privacy)-policy)(/)?"
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "file_server",
                                  "hide": [],
                                  "root": "/var/www/html/site"
                                }
                              ]
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "terminal": false
            }
          ],
          "errors": {
            "routes": [
              {
                "handle": [
                  {
                    "handler": "vars",
                    "status_code": "{http.error.status_code}"
                  }
                ]
              },
              {
                "match": [
                  {
                    "vars": {
                      "status_code": "404"
                    }
                  }
                ],
                "handle": [
                  {
                    "handler": "static_response",
                    "status_code": "{http.error.status_code}"
                  },
                  {
                    "handler": "rewrite",
                    "uri": "/404.html"
                  }
                ],
                "terminal": true
              },
              {
                "match": [
                  {
                    "not": {
                      "vars": {
                        "status_code": ""
                      }
                    }
                  }
                ],
                "handle": [
                  {
                    "handler": "static_response",
                    "status_code": "{http.error.status_code}"
                  }
                ],
                "terminal": true
              },
              {
                "handle": [
                  {
                    "handler": "static_response",
                    "status_code": "500"
                  }
                ],
                "terminal": true
              }
            ]
          }
        }
      }
    }
  },
  "logging": {
    "sink": {
      "writer": {
        "output": "file",
        "filename": "/var/log/caddy/std.log",
        "roll": true,
        "roll_size_mb": 2,
        "roll_gzip": false,
        "roll_local_time": false,
        "roll_keep": 5,
        "roll_keep_days": 10
      }
    },
    "logs": {
      "default": {
        "writer": {
          "output": "file",
          "filename": "/var/log/caddy/caddy.log",
          "roll": true,
          "roll_size_mb": 2,
          "roll_gzip": false,
          "roll_local_time": false,
          "roll_keep": 5,
          "roll_keep_days": 10
        }
      }
    }
  }
}

What happens is that while i see the correct 404 status code in postman/browser, the custom 404 page itself is not served, i just get empty content.

Any update here?

Yes, we’re discussing a better way to match on error codes here: v2: proposal: Matcher to compare primitive values · Issue #3051 · caddyserver/caddy · GitHub

Also, your static_response handler for 404 doesn’t write a body, hence the response is empty. Make sure to add a file_server handler to your route if you want to serve a file.

Thank you!

I managed to get this going with this;

{
  "match": [
    {
      "vars": {
        "status_code": "404"
      }
    }
  ],
  "handle": [
    {
      "handler": "rewrite",
      "uri": "/404.html"
    },
    {
      "handler": "file_server",
      "hide": [],
      "root": "/var/www/html/site"
    }
  ],
  "terminal": true
},

Following the idea from caddy_verbose_auth_fail_config1.json5 · GitHub

@francislavoie - thank you too :wink:

2 Likes

Excellent! That looks about right. Thanks for posting your solution, to help others.

We’ll make this easier sooner or later, so stay tuned.

1 Like

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