2025, Nov 30 06:01

Ошибка 404 после интеграции Flask-Login: исправляем user_loader

Почему Flask-Login с некорректным user_loader вызывает 404 в Flask. Разбор причины и решение на SQLAlchemy: возвращайте пользователя или None, а не get_or_404.

Когда приложение Flask внезапно начинает возвращать 404 Not Found после интеграции Flask-Login, первая мысль — винить маршрутизацию или шаблоны. Но на деле тонкая ошибка в колбэке user_loader может перехватывать запросы ещё до того, как они дойдут до представления. Ниже — минимальная конфигурация, показывающая, как одна строка в связующем коде аутентификации способна превратить каждый запрос в 404.

Как воспроизвести проблему

Приложение использует Flask, Flask-Login и SQLAlchemy с простой схемой в стиле блога. Проблема проявляется сразу после подключения Flask-Login и реализации user_loader, который выполняет поиск и поднимает 404, если пользователь не найден.

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')

Что на самом деле происходит

Flask-Login полагается на колбэк user_loader, чтобы превратить user_id из сессии в объект пользователя. Ожидание простое: вернуть экземпляр пользователя, если он найден, иначе вернуть None. Если же загрузчик во время этого поиска поднимает 404, обработка запроса прерывается ответом Not Found. Поэтому маршруты, которые раньше работали, будто исчезают сразу после подключения загрузчика.

Такое поведение соответствует рекомендациям из раздела «How it Works» документации Flask-Login: загрузчик должен выполнить запрос к базе данных и вернуть None, когда соответствующего пользователя нет. Возвращать 404 здесь и не нужно, и вредно, потому что при неудачном поиске это подменяет обычную обработку представления страницей ошибки — например, после сброса базы, когда пользователей ещё нет.

Решение

Замените поиск, который вызывает 404, на запрос, возвращающий None, если пользователя не существует. В SQLAlchemy надёжный вариант — получить запись через сессию, при необходимости приведя id.

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

Это изменение сохраняет корректный поток аутентификации: Flask-Login получает либо валидного пользователя, либо None. Если пользователь не найден, запрос продолжается без вошедшей личности вместо того, чтобы падать с 404. Если после этого пользователь всё равно не остаётся аутентифицированным, это указывает на отдельную проблему.

Зачем это знать

Использовать get_or_404 в загрузчике обманчиво удобно, но это стирает грань между аутентификацией и маршрутизацией. Как только таблица пользователей пустеет или устаревшая сессия ссылается на отсутствующую запись, приложение начинает возвращать 404 для несвязанных страниц. Это объясняет случаи, когда приложение «вдруг перестало работать» после сброса базы: загрузчик ничего не находит и отвечает 404, маскируя реальный маршрут. Держите загрузчик без побочных эффектов — так отказ становится явным и гораздо понятнее для диагностики.

Вывод

Сделайте так, чтобы ваш user_loader возвращал объект пользователя или None, и не поднимайте 404 внутри него. Это разводит логику аутентификации и маршрутизацию и предотвращает запутывающие ответы Not Found, когда запись пользователя отсутствует. Если после этого приложение всё ещё не сохраняет сессии, рассматривайте это как отдельное направление отладки и двигайтесь дальше оттуда.