{"id":"d3277e0b0fd5869d","repo":"pallets/flask","slug":"a-request-was-sent-to-request-url-but-routing","errorCode":null,"errorMessage":"A request was sent to '{request.url}', but routing issued a redirect to the canonical URL '{exc.new_url}'. The URL was defined with a trailing slash. Flask will redirect to the URL with a trailing slash if it was accessed without one. Send requests to the canonical URL, or use 307 or 308 for routing redirects. Otherwise, browsers will drop form data.\n\nThis exception is only raised in debug mode.","messagePattern":"A request was sent to '(.+?)', but routing issued a redirect to the canonical URL '(.+?)'\\. The URL was defined with a trailing slash\\. Flask will redirect to the URL with a trailing slash if it was accessed without one\\. Send requests to the canonical URL, or use 307 or 308 for routing redirects\\. Otherwise, browsers will drop form data\\.\n\nThis exception is only raised in debug mode\\.","errorType":"exception","errorClass":"FormDataRoutingRedirect","httpStatus":null,"severity":"error","filePath":"src/flask/app.py","lineNumber":588,"sourceCode":"        body.\n\n        .. versionchanged:: 2.1\n            Don't intercept 307 and 308 redirects.\n\n        :meta private:\n        :internal:\n        \"\"\"\n        if (\n            not self.debug\n            or not isinstance(request.routing_exception, RequestRedirect)\n            or request.routing_exception.code in {307, 308}\n            or request.method in {\"GET\", \"HEAD\", \"OPTIONS\"}\n        ):\n            raise request.routing_exception  # type: ignore[misc]\n\n        from .debughelpers import FormDataRoutingRedirect\n\n        raise FormDataRoutingRedirect(request)\n\n    def update_template_context(\n        self, ctx: AppContext, context: dict[str, t.Any]\n    ) -> None:\n        \"\"\"Update the template context with some commonly used variables.\n        This injects request, session, config and g into the template\n        context as well as everything template context processors want\n        to inject.  Note that the as of Flask 0.6, the original values\n        in the context will not be overridden if a context processor\n        decides to return a value with the same key.\n\n        :param context: the context as a dictionary that is updated in place\n                        to add extra variables.\n        \"\"\"\n        names: t.Iterable[str | None] = (None,)\n\n        # A template may be rendered outside a request context.\n        if ctx.has_request:","sourceCodeStart":570,"sourceCodeEnd":606,"githubUrl":"https://github.com/pallets/flask/blob/6a2f545bfd8ed31e19066a299296917e034aca58/src/flask/app.py#L570-L606","documentation":"This is `FormDataRoutingRedirect`, raised by `Flask.raise_routing_exception` (src/flask/app.py:562) in debug mode when Werkzeug's router issues a non-307/308 `RequestRedirect` (e.g. 301 to the trailing-slash canonical URL) for a request whose method can carry a body (POST/PUT/etc.). Following such a redirect would make browsers switch to GET and drop the form data, so Flask surfaces a loud error instead of a silent data loss. Modern Werkzeug uses 308 redirects, which preserve method and body, so this mainly appears with older Werkzeug or custom rules.","triggerScenarios":"`app.debug` is True, `request.routing_exception` is a `RequestRedirect` with a code other than 307/308, and the method is not GET/HEAD/OPTIONS — e.g. POSTing to `/items` when the route is defined as `/items/` (strict_slashes redirect) under an older Werkzeug, or custom routing rules/redirect_defaults producing 301/302.","commonSituations":"Defining routes with trailing slashes (`@app.route('/submit/')`) and POSTing to the slashless URL from JS or tests; pinned old Werkzeug versions (<2.1 behavior); reverse proxies rewriting paths so the app sees the non-canonical URL.","solutions":["Send the request to the canonical URL exactly as defined (include the trailing slash).","Upgrade Werkzeug so routing redirects use 308, which preserves the method and body.","Define the route without a trailing slash, or set `strict_slashes=False` on the rule, if you want both forms to work: `@app.route('/submit', strict_slashes=False)`."],"exampleFix":"# before (route is /submit/, client posts to /submit)\nfetch(\"/submit\", {method: \"POST\", body: data})\n# after\nfetch(\"/submit/\", {method: \"POST\", body: data})","handlingStrategy":"validation","validationCode":"# send requests to the canonical URL (trailing slash matches the route definition)\nresp = client.post('/items/', data=payload)  # route defined as '/items/'","typeGuard":null,"tryCatchPattern":"try:\n    resp = client.post('/items', data=payload)\nexcept Exception as e:  # flask.app raises only in debug mode\n    if 'routing issued a redirect' in str(e):\n        # fix the caller to use the canonical URL\n        raise","preventionTips":["Always generate URLs with url_for() instead of hard-coding paths — it yields the canonical form","Match trailing slashes to the route definition in clients, tests, and frontend fetch calls","For APIs, consider strict_slashes=False or defining routes without trailing slashes to avoid redirects that drop POST bodies"],"tags":["flask","routing","redirect","trailing-slash","debug-mode","forms"],"analyzedSha":"6a2f545bfd8ed31e19066a299296917e034aca58","analyzedAt":"2026-07-31T19:13:49.921Z","schemaVersion":2}