Skip to content

Fix: Flask Route Returns 404 Not Found

FixDevs ·

Quick Answer

How to fix Flask routes returning 404 — trailing slash redirect, Blueprint prefix issues, route not registered, debug mode, and common URL rule mistakes.

The Error

A Flask route that should exist returns a 404:

GET /api/users HTTP/1.1
404 NOT FOUND

Or Flask returns a redirect instead of the expected response:

GET /api/users/
301 MOVED PERMANENTLY → /api/users

Or the route works in development but not after deployment. Or a Blueprint route is completely unreachable:

@users_bp.route('/profile')
def profile():
    return jsonify({'user': 'Alice'})

# GET /profile → 404
# GET /users/profile → 404
# No combination works

Why This Happens

  • Trailing slash mismatch — Flask has strict URL rules. /users and /users/ are different routes. Flask’s default behavior redirects /users/ to /users (or returns 404 for the inverse), depending on how the route is defined.
  • Blueprint not registered on the app — defining a Blueprint doesn’t make its routes available. You must call app.register_blueprint().
  • Blueprint url_prefix applied unexpectedly — routes defined as /profile in a Blueprint registered with url_prefix='/users' are reachable at /users/profile, not /profile.
  • Route defined after app.run() — Flask registers routes at import time. Any route decorated after app.run() is never registered.
  • Module not imported — if the file containing a route is never imported, Flask never sees the decorator.
  • Method not allowed — the route exists but only handles GET, and you’re sending a POST request. Flask returns 405, not 404, but the behavior feels the same.
  • Static files interfering — a static file at the same path as a route can shadow the route in some configurations.

Fix 1: Fix Trailing Slash Rules

Flask distinguishes between routes with and without a trailing slash:

# Defined WITHOUT trailing slash
@app.route('/users')
def get_users():
    return 'users'

# GET /users → 200 OK ✓
# GET /users/ → 308 PERMANENT REDIRECT → /users
# Defined WITH trailing slash (directory-like)
@app.route('/users/')
def get_users():
    return 'users'

# GET /users/ → 200 OK ✓
# GET /users → 301 REDIRECT → /users/

Best practice for APIs — omit trailing slashes and set strict_slashes=False:

@app.route('/api/users', strict_slashes=False)
def get_users():
    return jsonify({'users': []})

# Both /api/users and /api/users/ → 200 OK ✓

Or configure globally for all routes:

app = Flask(__name__)
app.url_map.strict_slashes = False  # All routes accept with or without trailing slash

Fix 2: Register the Blueprint

A Blueprint’s routes are only active after the Blueprint is registered with the app:

# users/routes.py
from flask import Blueprint, jsonify

users_bp = Blueprint('users', __name__)

@users_bp.route('/profile')
def profile():
    return jsonify({'user': 'Alice'})
# app.py
from flask import Flask
from users.routes import users_bp  # Import the Blueprint

app = Flask(__name__)

# MUST register the Blueprint — routes are inactive without this
app.register_blueprint(users_bp, url_prefix='/users')

# Now reachable at /users/profile

Verify registered routes:

# List all registered routes — add this temporarily to debug
with app.app_context():
    for rule in app.url_map.iter_rules():
        print(f"{rule.methods} {rule.rule}")
# Or use the Flask CLI
flask routes
# Endpoint          Methods    Rule
# ----------------  ---------  -------------------------
# users.profile     GET, HEAD  /users/profile
# static            GET        /static/<path:filename>

Fix 3: Fix Blueprint url_prefix

The url_prefix in register_blueprint() is prepended to every route in the Blueprint:

# Blueprint defines /profile
@users_bp.route('/profile')
def profile():
    return 'profile'

# Registered with prefix /users
app.register_blueprint(users_bp, url_prefix='/users')

# Route is at /users/profile — NOT /profile

You can also set the prefix on the Blueprint itself:

users_bp = Blueprint('users', __name__, url_prefix='/users')

@users_bp.route('/profile')  # Full path: /users/profile
def profile():
    return 'profile'

# Register without a prefix (Blueprint already has one)
app.register_blueprint(users_bp)

Don’t double-prefix:

# Wrong — prefix on both Blueprint and register_blueprint
users_bp = Blueprint('users', __name__, url_prefix='/users')

@users_bp.route('/profile')
def profile():
    return 'profile'

# Registered with ANOTHER prefix — full path: /api/users/profile (double prefix)
app.register_blueprint(users_bp, url_prefix='/api/users')

Fix 4: Ensure Modules Are Imported

Flask only knows about routes that have been decorated and imported. If a module with route definitions is never imported, those routes don’t exist:

# app.py — WRONG: routes never imported
from flask import Flask

app = Flask(__name__)

if __name__ == '__main__':
    app.run()

# users.py exists with routes but is never imported → 404
# app.py — CORRECT: import routes after creating app
from flask import Flask

app = Flask(__name__)

# Import routes to register them (even if you don't use the module directly)
from . import users   # or: import users

if __name__ == '__main__':
    app.run()

Application factory pattern — import routes inside the factory:

def create_app():
    app = Flask(__name__)

    # Register Blueprints inside the factory
    from .users import users_bp
    from .posts import posts_bp

    app.register_blueprint(users_bp, url_prefix='/users')
    app.register_blueprint(posts_bp, url_prefix='/posts')

    return app

Fix 5: Fix Method Not Allowed (405 vs 404)

By default, Flask routes only accept GET (and HEAD). Sending a POST to a GET-only route returns 405, which can look like a 404:

# Only handles GET
@app.route('/api/users')
def get_users():
    return jsonify([])

# POST /api/users → 405 Method Not Allowed
# Handle multiple methods explicitly
@app.route('/api/users', methods=['GET', 'POST'])
def users():
    if request.method == 'GET':
        return jsonify([])
    elif request.method == 'POST':
        data = request.json
        return jsonify(data), 201

Or use method-specific decorators (Flask 2.0+):

@app.get('/api/users')
def get_users():
    return jsonify([])

@app.post('/api/users')
def create_user():
    data = request.json
    return jsonify(data), 201

Check what methods a route accepts:

flask routes
# Endpoint     Methods          Rule
# -----------  ---------------  ------------
# get_users    GET, HEAD        /api/users

Fix 6: Debug Route Registration at Runtime

Use Flask’s built-in tools to see exactly what routes are registered:

# In your app or a debug script
from app import app

with app.test_request_context():
    print(app.url_map)
# Test a specific URL
from app import app

with app.test_request_context('/api/users'):
    from flask import request
    print(request.path)  # /api/users

# Match a URL to a route
from werkzeug.routing import Map
adapter = app.url_map.bind('localhost')
try:
    endpoint, args = adapter.match('/api/users', method='GET')
    print(f"Route: {endpoint}, Args: {args}")
except Exception as e:
    print(f"No match: {e}")

Enable debug mode to get detailed error pages:

app = Flask(__name__)
app.debug = True  # Shows full traceback on 500, detailed routing info

# Or via environment variable
# FLASK_DEBUG=1 flask run

Warning: Never run Flask with debug=True in production. Debug mode enables the interactive debugger, which allows arbitrary code execution if accessed by an attacker.

Fix 7: Fix Routes After Deployment

Routes that work locally but return 404 after deployment are usually caused by:

WSGI server path prefix issues:

# If your app is mounted at /myapp on the server (not /)
# Requests arrive at /myapp/api/users but Flask sees /api/users
# Use APPLICATION_ROOT or ProxyFix middleware

from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1)

Gunicorn with a different module path:

# Correct — specify module:app
gunicorn "myapp:create_app()"  # For application factory
gunicorn myapp.app:app         # For direct app object

# Wrong — module not found, Flask defaults to 404 for all routes
gunicorn myapp:application     # If variable is named 'app', not 'application'

Nginx stripping the path prefix:

# Wrong — strips /api prefix before passing to Flask
location /api {
    proxy_pass http://localhost:5000;
    # Request for /api/users becomes /users in Flask → 404
}

# Correct — preserve the full path
location /api {
    proxy_pass http://localhost:5000/api;
}

# Or use a trailing slash on both sides
location /api/ {
    proxy_pass http://localhost:5000/api/;
}

Still Not Working?

Print all routes at startup:

@app.before_request
def log_request():
    app.logger.debug(f"Request: {request.method} {request.path}")

Check if the route is being shadowed by a static file:

# Flask serves static files from /static by default
# A file at static/api/users.html would NOT shadow /api/users routes
# But misconfigured web servers can serve static files for all paths

ls app/static/

Try the route with Flask’s test client to bypass network issues:

def test_route():
    with app.test_client() as client:
        response = client.get('/api/users')
        print(response.status_code)   # Should be 200, not 404
        print(response.data)

For related issues, see Fix: FastAPI 422 Unprocessable Entity and Fix: Express Cannot GET Route.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles