2025, Nov 20 17:00

Flask-Login causing 404 on every route? The user_loader get_or_404 pitfall and the right SQLAlchemy fix

Learn why integrating Flask-Login can trigger 404 Not Found when user_loader uses get_or_404, and how to fix it with SQLAlchemy session.get by returning None.

When a Flask app suddenly starts returning 404 Not Found after integrating Flask-Login, the first instinct is to blame routing or templates. But in practice, a subtle mistake in the user_loader callback can short-circuit your requests before they ever reach a view. Below is a minimal setup illustrating how a single line in the authentication glue can turn every request into a 404.

Reproducing the issue

The application uses Flask, Flask-Login, and SQLAlchemy with a simple blog-style schema. The problem surfaces as soon as Flask-Login is plugged in and the user_loader is implemented with a lookup that raises a 404 when the user is missing.

from flask import Flask, render_template, request
from flask_login import UserMixin, LoginManager, current_user
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import relationship, DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, Text, ForeignKey
from typing import List
webapp = Flask(__name__)
webapp.config['SECRET_KEY'] = '8BYkEfBA6O6donzWlSihBXox7C0sKR6b'
auth_manager = LoginManager()
auth_manager.init_app(webapp)
@auth_manager.user_loader
def fetch_identity(user_id):
    return store.get_or_404(Account, user_id)
class ModelBase(DeclarativeBase):
    pass
webapp.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///posts.db'
store = SQLAlchemy(model_class=ModelBase)
store.init_app(webapp)
class Article(store.Model):
    __tablename__ = "articles"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
    subtitle: Mapped[str] = mapped_column(String(250), nullable=False)
    date: Mapped[str] = mapped_column(String(250), nullable=False)
    body: Mapped[str] = mapped_column(Text, nullable=False)
    writer = relationship("Account", back_populates='entries')
    image_link: Mapped[str] = mapped_column(String(250), nullable=False)
    writer_id = store.Column(Integer, ForeignKey('members.id'))
class Account(UserMixin, store.Model):
    __tablename__ = "members"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    email: Mapped[str] = mapped_column(String, unique=True, nullable=False)
    password: Mapped[str] = mapped_column(String, nullable=False)
    name: Mapped[str] = mapped_column(String, nullable=False)
    entries: Mapped[List["Article"]] = relationship(back_populates="writer")
with webapp.app_context():
    store.create_all()
@webapp.route("/")
def index():
    return render_template('test.html')

What actually goes wrong

Flask-Login relies on the user_loader callback to turn a user_id from the session into a user object. The expectation is simple: return a user instance if found, otherwise return None. If, instead, the loader raises a 404 during this lookup, the request handling is aborted with a Not Found response. That’s why routes that previously worked appear to vanish as soon as you wire in the loader.

This behavior aligns with the guidance in the “How it Works” section of the Flask-Login documentation: the loader should perform a database query and yield None when no matching user exists. Returning a 404 here is unnecessary and harmful because it replaces your normal view handling with an error page whenever the lookup fails—such as after a database reset, when no users exist yet.

The fix

Replace the 404-triggering lookup with a query that returns None when the user does not exist. In SQLAlchemy, a safe choice is to fetch via the session and cast the id as needed.

@auth_manager.user_loader
def fetch_identity(user_id):
    return store.session.get(Account, int(user_id))

This change keeps the authentication flow correct: Flask-Login receives either a valid user or None. If the user isn’t found, the request proceeds without a logged-in identity instead of failing with a 404. If the user still doesn’t remain authenticated after this change, that indicates a separate issue.

Why you want to know this

Using get_or_404 in a loader is deceptively convenient, but it blurs the line between authentication and routing. As soon as the user table is empty or a stale session points to a missing record, your app starts returning 404 for unrelated pages. That explains scenarios where the app “suddenly stopped working” after a database reset: the loader finds nothing and responds with a 404, masking the real route. Keeping the loader side-effect free makes failures explicit and much easier to reason about.

Conclusion

Make your user_loader return a user object or None, and avoid raising 404 from within it. This keeps authentication concerns separate from request routing and prevents confusing Not Found responses when the user record isn’t present. If the application still fails to persist sessions afterward, treat that as a different debugging track and proceed from there.