Max_size of request body doesn't respond with HTTP 413 code

1. Caddy version (caddy version):

v2.1.1 h1:X9k1+ehZPYYrSqBvf/ocUgdLSRIuiNiMo7CvyGUQKeA=

2. How I run Caddy:

a. System environment:

Docker, caddy:2-alpine image

b. Command:

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

c. Service/unit/compose file:

N/A

d. My complete Caddyfile or JSON config:

{
  "admin": {
    "config": {
      "persist": false
    },
    "enforce_origin": false,
    "listen": "0.0.0.0:2019",
    "origins": []
  },
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "errors": {
            "routes": [
               {
                  "handle": [
                       {
                           "error": "Payload Too Large",
                           "handler": "error",
                           "status_code": "413"
                       }
                   ],
                   "match": [
                       {
                           "expression": "{http.request.header.Content-Length} > 15728640"
                       }
                   ]
                }
            ]
          },
          "listen": [
            ":443"
          ],
          "logs": {
            "default_logger_name": "log0"
          },
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "static_response",
                          "headers": {
                            "Location": [
                              "{http.request.uri.query.target}"
                            ]
                          },
                          "status_code": 302
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/site-redirect*"
                          ],
                          "query": {
                            "target": [
                              "*"
                            ]
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "reverse_proxy",
                          "transport": {
                            "protocol": "http"
                          },
                          "upstreams": [
                            {
                              "dial": "localhost:6667"
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/api/pub/*",
                            "/api/gateway/*",
                            "/auth/oauth/*"
                          ]
                        }
                      ]
                    }
                  ]
                },
                {
                  "@id": "newroutes",
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "request_body",
                          "max_size": 15728640
                        },
                        {
                          "handler": "file_server",
                          "pass_thru": true,
                          "root": "public"
                        }
                      ],
                      "match": [
                        {
                          "path_regexp": {
                            "name": "newroutes_regexp1",
                            "pattern": "(?i)^/public/*"
                          }
                        },
                        {
                          "path_regexp": {
                            "pattern": "(?i)^/(robots|recaptcha|sitemap).*"
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "request_body",
                          "max_size": 15728640
                        },
                        {
                          "handler": "file_server",
                          "pass_thru": true,
                          "root": "v2app"
                        }
                      ],
                      "match": [
                        {
                          "path_regexp": {
                            "pattern": "(?i)^/((default|common|polyfills|root_shared|runtime|styles|ng-assets|shared-assets|assets).*)$"
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "request_body",
                          "max_size": 15728640
                        },
                        {
                          "handler": "rewrite",
                          "uri": "/{path}"
                        }
                      ],
                      "match": [
                        {
                          "path_regexp": {
                            "pattern": "^(?i)(^/$)|((/login|/account-summary).*$)"
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "request_body",
                          "max_size": 15728640
                        },
                        {
                          "handler": "file_server",
                          "hide": [
                            "/etc/caddy/caddy_config.json"
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "localhost"
                  ]
                }
              ],
              "terminal": true
            }
          ],
          "tls_connection_policies": [
            {
              "certificate_selection": {
                "any_tag": [
                  "mycert"
                ]
              }
            }
          ]
        }
      }
    },
    "tls": {
      "certificates": {
        "load_files": [
          {
            "certificate": "/etc/tls/tls.crt",
            "key": "/etc/tls/tls.key",
            "tags": [
              "mycert"
            ]
          }
        ]
      }
    }
  },
  "logging": {
    "logs": {
      "default": {
        "exclude": [
          "http.log.access.log0"
        ],
        "level": "warn"
      },
      "log0": {
        "encoder": {
          "format": "json"
        },
        "include": [
          "http.log.access.log0"
        ],
        "level": "info",
        "writer": {
          "output": "stdout"
        }
      }
    }
  }
}

3. The problem I’m having:

I want to put a max_size on any request body and want Caddy to respond with an HTTP 413 code (like a web server should), but I noticed that the code for max_size of the request body handler only limits the body size for the next handler. There isn’t any documentation on how to mimic servers such as nginx or apache where the max body size is setting where they generate a 413.

4. Error messages and/or full log output:

Oddly, no amount of artificially generating large requests will generate an HTTP 413 error. If I use Postman to add a 30MB file into a post playload to the login page, the login page happily responds with a HTTP 200 which is not what I would expect. Are there examples somewhere online of using the errors block to capture when the max_size of a request has exceeded the set number of bytes and intercept the response back to the client?

5. What I already tried:

I have tried to tinker with the configuration above, but nothing about Caddy’s behavior changes.

6. Links to relevant resources:

Hey @kernelpanek, welcome to the Caddy community.

I don’t think any of the HTTP handlers will generate an error for that right now in code. Might be worth a feature request. But you can indeed generate your own error - if your client is well-behaved - based on the Content-Length request header… which it looks like you’re doing already.

I note that http.handlers.error generates an error but does not actually write a response. You’d want to configure error handling that implements a static_response handler to actually… well, send something to the client based on that error you’ve raised.

Modules - Caddy Documentation
Modules - Caddy Documentation

1 Like

Uhh, to follow up a bit on this, you’ll need to do a bit of rearranging as well.

You want to match for the long Content-Length somewhere in your routes, not under errors. We don’t get to the errors section until something in routes… well, throws an error.

So you need to put a matcher in your routes that catches the Content-Length. You can then either:

  • Use the error handler to generate an error, then handle it in errors with a static_response handler, or;
  • Just issue a static_response right then and there instead of bothering with generating an error
2 Likes

You rock @Whitestrake!!

I had been thinking about the errors and routes a bit backwards, so based on your help I updated my config to this and it worked!

{
  "admin": {
    "config": {
      "persist": false
    },
    "enforce_origin": false,
    "listen": "0.0.0.0:2019",
    "origins": []
  },
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "errors": {
            "routes": [{
              "handle": [{
                "handler": "static_response",
                "status_code": "413",
                "body": "Payload Too Large",
                "close": true
              }],
              "match": [{
                "vars": {
                  "status_code": "404"
                }
              }]
            }]
          },
          "listen": [
            ":443"
          ],
          "logs": {
            "default_logger_name": "log0"
          },
          "routes": [{
            "handle": [{
                "handler": "subroute",
                "routes": [{
                    "handle": [{
                      "error": "Payload Too Large",
                      "handler": "error",
                      "status_code": "413"
                    }],
                    "match": [{
                      "expression": "int({http.request.header.Content-Length}) > 15728640"
                    }]
                  },
                  {
                    "handle": [{
                      "handler": "static_response",
                      "headers": {
                        "Location": [
                          "{http.request.uri.query.target}"
                        ]
                      },
                      "status_code": 302
                    }],
                    "match": [{
                      "path": [
                        "/site-redirect*"
                      ],
                      "query": {
                        "target": [
                          "*"
                        ]
                      }
                    }]
                  },
                  {
                    "handle": [{
                      "handler": "reverse_proxy",
                      "transport": {
                        "protocol": "http"
                      },
                      "upstreams": [{
                        "dial": "localhost:6667"
                      }]
                    }],
                    "match": [{
                      "path": [
                        "/api/pub/*",
                        "/api/gateway/*",
                        "/auth/oauth/*"
                      ]
                    }]
                  }
                ]
              },
              {
                "@id": "newroutes",
                "handler": "subroute",
                "routes": [{
                    "handle": [{
                      "handler": "file_server",
                      "pass_thru": true,
                      "root": "public"
                    }],
                    "match": [{
                        "path_regexp": {
                          "name": "newroutes_regexp1",
                          "pattern": "(?i)^/public/*"
                        }
                      },
                      {
                        "path_regexp": {
                          "pattern": "(?i)^/(robots|recaptcha|sitemap).*"
                        }
                      }
                    ]
                  },
                  {
                    "handle": [{
                      "handler": "file_server",
                      "pass_thru": true,
                      "root": "v2app"
                    }],
                    "match": [{
                      "path_regexp": {
                        "pattern": "(?i)^/((default|common|polyfills|root_shared|runtime|styles|ng-assets|shared-assets|assets).*)$"
                      }
                    }]
                  },
                  {
                    "handle": [{
                      "handler": "rewrite",
                      "uri": "/{path}"
                    }],
                    "match": [{
                      "path_regexp": {
                        "pattern": "^(?i)(^/$)|((/login|/account-summary).*$)"
                      }
                    }]
                  },
                  {
                    "handle": [{
                      "handler": "file_server",
                      "hide": [
                        "/etc/caddy/caddy_config.json"
                      ]
                    }]
                  }
                ]
              }
            ],
            "match": [{
              "host": [
                "localhost"
              ]
            }],
            "terminal": true
          }],
          "tls_connection_policies": [{
            "certificate_selection": {
              "any_tag": [
                "mycert"
              ]
            }
          }]
        }
      }
    },
    "tls": {
      "certificates": {
        "load_files": [{
          "certificate": "/etc/tls/tls.crt",
          "key": "/etc/tls/tls.key",
          "tags": [
            "mycert"
          ]
        }]
      }
    }
  },
  "logging": {
    "logs": {
      "default": {
        "exclude": [
          "http.log.access.log0"
        ],
        "level": "warn"
      },
      "log0": {
        "encoder": {
          "format": "json"
        },
        "include": [
          "http.log.access.log0"
        ],
        "level": "info",
        "writer": {
          "output": "stdout"
        }
      }
    }
  }
}
2 Likes

It might not be the most secure implementation as I’m sure someone could craft an HTTP POST without courteously setting the Content-Length, but this will work for the common, browser-originating POSTs.

Thanks again for your help! I am considering putting up a feature request and possibly a PR for optionally allowing the request_body to generate an error of one’s choice. The maxBytesReader.Read() function might be a more trustworthy place to interrupt the reqeuest->response process.

1 Like

Glad to help! (Thanks to @francislavoie chatting behind the scenes on this issue, too.)

That does sound like a neat little feature enhancement for the request_body handler, one that will allow for a guarantee that the request body limit can’t be abused. We’d love to see that PR :smiley:

2 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.