Fix: Flask Route Returns 404 Not Found
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 FOUNDOr Flask returns a redirect instead of the expected response:
GET /api/users/
301 MOVED PERMANENTLY → /api/usersOr 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 worksWhy This Happens
- Trailing slash mismatch — Flask has strict URL rules.
/usersand/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_prefixapplied unexpectedly — routes defined as/profilein a Blueprint registered withurl_prefix='/users'are reachable at/users/profile, not/profile. - Route defined after
app.run()— Flask registers routes at import time. Any route decorated afterapp.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 aPOSTrequest. 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 slashFix 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/profileVerify 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 /profileYou 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 appFix 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), 201Or 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), 201Check what methods a route accepts:
flask routes
# Endpoint Methods Rule
# ----------- --------------- ------------
# get_users GET, HEAD /api/usersFix 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 runWarning: Never run Flask with
debug=Truein 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Python dataclass Mutable Default Value Error (ValueError / TypeError)
How to fix Python dataclass mutable default errors — why lists, dicts, and sets cannot be default field values, how to use field(default_factory=...), and common dataclass pitfalls with inheritance and ClassVar.
Fix: Python requests.get() Hanging — Timeout Not Working
How to fix Python requests hanging forever — why requests.get() ignores timeout, how to set connect and read timeouts correctly, use session-level timeouts, and handle timeout exceptions properly.
Fix: AWS ECS Task Failed to Start
How to fix ECS tasks that fail to start — port binding errors, missing IAM permissions, Secrets Manager access, essential container exit codes, and health check failures.
Fix: Docker Multi-Stage Build COPY --from Failed
How to fix Docker multi-stage build errors — COPY --from stage not found, wrong stage name, artifacts not at expected path, and BuildKit caching issues.