Обработка ошибок доступа web-приложения на Python и Starlette

programming

Опубликован:
2024-12-10T22:40:17.694238Z
Отредактирован:
2024-12-10T22:40:17.694238Z
Статус:
публичный
19
0
0

Web-приложение на Python и Starlette, разработку которого мы исследуем в этом цикле статей, уже в ближайшей перспективе получит некоторое множество url-адресов, на которых сервер будет слушать сеть и принимать от пользователей сети HTTP запросы. Эти url-адреса в процессе разработки программы я представлю на страницах приложения соответствующими ссылками, уже скоро, и, таким образом, они станут доступны посетителям будущего сайта. И здесь кроется одна небольшая проблема... Не все пользователи сети строго следуют по представленным на страницах сайта ссылкам. Иногда некоторые из них отправляют на сервер хаотичные запросы по одному богу известно откуда взятым url-адресам. В этой демонстрации я обозначу некоторые пути решения этой проблемы.

Моделируем ошибочные действия пользователя сайта

Допустим ситуацию, когда пользователь сети заходит на web-сайт по ссылке на каком-нибудь другом сайте. Такое может случиться, если используемый для сайта домен имеет историю, был использован кем-то до вас, а прежний владелец домена накидал по сети ссылок на различных форумах, в блогах, социальных сетях — одна из гипотетически возможных ситуаций. Допустим, где-то в сети до сих пор хранится ссылка следующего вида: http://localhost:5000/babam. Здесь вместо localhost:5000 следует вставить имя только что зарегистрированного вами домена.

Смоделируем ситуацию в браузере... Запускаю терминал, вхожу в корневой каталог приложения, активирую виртуальное окружение и запускаю отладочный сервер. В соседнем окне запускаю браузер, в его адресной строке вбиваю заведомо несуществующий в карте адресов приложения url и жму enter.

MmYpiDbX9N.png

Как видно на снимке экрана выше, сервер ответил на запрос браузера, в этом ответе содержится простой текст, одна единственная строка — Not found. Увы, но для конечного пользователя на такой странице нет никакой более или менее привлекательной, побуждающей его остаться на сайте информации. Что будет делать пользователь в этом случае? Правильно, он закроет вкладку и пойдёт на любой другой известный ему сайт с интересным контентом. Ситуация для web-мастера печальная...

Для начала давайте обратим своё пристальное внимание на окно терминала, в котором отладочный сервер пишет свой неутешительный отчёт.

4C6kqEQCKo.png

Как видно на снимке экрана выше, в отчёте числится код HTTP ошибки 404, но при этом в выхлопе сервера нет ошибок исполнения и не содержится соответствующих отчётов интерпретатора. Это говорит нам, что ошибка порождена инструментами Starlette, и несуществующий в карте приложения url обрабатывается штатным обработчиком где-то в недрах этого великолепного на мой взгляд web-фреймворка. И сейчас я попытаюсь ситуацию обернуть в пользу web-мастера будущего сайта.

Дополнительная конфигурация ASGI

Как мы помним из предыдущих выпусков этого цикла статей, конфигурация ASGI-приложения хранится в "ините" базового каталога. Открываю этот файл в текстовом редакторе Vim.

$ vim webapp/__init__.py

Мне нужен собственный обработчик HTTP ошибок, его у меня пока нет, я создам его чуть-чуть позже, но в файл с конфигурацией я его обязан подключить. Дописываю в этот "инит" ещё одну процедуру импорта.

from .errors import show_error

По содержанию этой процедуры несложно догадаться, что пока несуществующий объект с именем show_error из пока несуществующего модуля .errors и предполагается использовать в качестве обработчика HTTP ошибок. Обращаю ваше внимание, что процедуры импорта принято группировать в следующем порядке: сначала следуют процедуры импорта из модулей стандартной библиотеки, затем процедуры импорта из модулей сторонних библиотек, загруженных с PyPI, затем следуют процедуры импорта из спроектированных собственноручно программных модулей. Процедуры импорта я обычно упорядочиваю ещё и по вложенности, и по алфавиту. Эти простые правила качественно облегчают жизнь другим участникам проекта, когда их несколько. Соответственно, предложенный импорт я вписываю под процедуру импорта объектов из модуля .dirs.

Web-приложение будет поступательно развиваться, и поэтому я предусмотрительно обозначаю коды HTTP ошибок, которые планирую обрабатывать предложенным обработчиком. На текущий момент меня интересуют коды 403, 404 и 405. Создаю в файле конфигурации следующий словарь:

errs = {403: show_error,
        404: show_error,
        405: show_error}

В процедуре инициализации экземпляра класса StApp, эта процедура уже существует в редактируемом файле, дописываю ещё один параметр с именем exception_handlers.

app = StApp(
    debug=settings.get('DEBUG', cast=bool),
    routes=[...],
    exception_handlers=errs)

Вот как выглядит код в окне моего текстового редактора.

zdFp7ddzbV.png

Чтобы трюк сработал, следующим шагом мне необходимо создать модуль .errors и в нём функцию представления с именем show_error.

Разработка подпрограммы errors

Прямо в окне текстового редактора, в его командном режиме создаю в базовом каталоге приложения новый вложенный каталог с именем errors, в нём я планирую хранить сценарии новой подпрограммы приложения.

:!mkdir webapp/errors/

И в этот каталог вкладываю файл с именем __init__.py.

:tabnew webapp/errors/__init__.py

Пишу в этот файл следующий код.

from starlette.responses import JSONResponse

E404 = 'Такой страницы у нас нет.'


async def show_error(request, exc):
    if exc.status_code == 403:
        exc.detail = 'Доступ ограничен, недостаточно прав.'
    if exc.status_code == 404:
        exc.detail = E404
    if exc.status_code == 405:
        exc.detail = 'Метод не позволен.'
    if request.method == 'GET':
        return request.app.jinja.TemplateResponse(
            'errors/error.html',
            {'reason': exc.detail,
             'request': request,
             'error': exc.status_code},
            status_code=exc.status_code)
    else:
        res = {'message': exc.detail,
               'error': exc.status_code}
        return JSONResponse(res)

В этом коде всё просто. Для каждого типа HTTP ошибки я задаю своё информационное сообщение — объект exc.detail. На запросы по методу GET я возвращаю полученную из обработанного с помощью инструментов Jinja2 шаблона html-страницу. Шаблон, который я использую, пока тоже не существует, но я его очень скоро создам. На все остальные, отличные от GET запросы, я возвращаю с помощью соответствующего библиотечного класса — JSONResponse — объект JSON заданного переменной res содержания — это задел на будущее, который будет использован на последующих этапах разработки этого web-приложения, когда в арсенале программы появятся запросы с методами POST, PUT, DELETE etc.

Естественно, раз я вписал имя шаблона в коде функции представления, шаблон с таким именем необходимо создать. Шаблоны хранятся в каталоге с именем templates, вложенном в базовый каталог приложения. И поскольку шаблон я намерен создать для подпрограммы errors, в каталоге templates я создаю новый вложенный каталог и даю ему имя подпрограммы — errors.

:!mkdir webapp/templates/errors

И уже в этом каталоге я создаю файл нового шаблона, в полном соответствии с кодом функции show_error. В командном режиме текстового редактора вбиваю следующую команду.

:tabnew webapp/templates/errors/error.html

В этот шаблон пишу следующий код...

Делаю его наследником базового шаблона.

{% extends "base.html" %}

Переопределяю блок title.

{% block title %}
  <title>{{ error }}</title>
{% endblock title %}

Здесь следует обратить внимание на переменную error — она передана в шаблон функцией представления show_error, и в ней хранится статус код обрабатываемого запроса. Впоследствии я ожидаю увидеть этот статус код в заголовке окна браузера.

Переопределяю блок styles. В этом блоке размещаю собственный assets, который отличается от соответствующего блока стартовой страницы только набором обрабатываемых .css файлов и адресом генерируемого файла в параметре output.

{% block styles %}
  {{ super() }}
  {% assets filters='cssmin', output='generic/css/errors/error.css',
            'css/base.css',
            'css/page-content.css',
            'css/error.css' %}
    <link rel="stylesheet" href="{{ ASSET_URL }}">
  {% endassets %}
{% endblock styles %}

Файлы base.css и page-content.css уже существуют, и их я изменять пока не планирую, а файл errors.css только ещё предстоит создать.

Получаемая из этого шаблона html-страница должна иметь собственное содержимое, для этого переопределяю блок page_body.

{% block page_body %}
  <h1>{{ error }}</h1>
  <img alt="error"
       src="{{ url_for('static', path='images/error.png') }}"
       width="96"
       height="96">
  <p>{{ reason }}</p>
{% endblock page_body %}

Обращаю ваше внимание, что в одном из элементов этой разметки я использую адрес изображения, которого в структуре каталогов web-приложения пока не существует.

Блок scripts этого шаблона тоже требует изменений. Переопределяю его.

{% block scripts %}
  {{ super() }}
  {% assets filters='rjsmin', output='generic/js/errors/error.js',
            'js/check-pc.js',
            'js/errors/error.js' %}
    <script src="{{ ASSET_URL }}"></script>
  {% endassets %}
{% endblock scripts %}

Здесь следует обратить внимание, что в списке файлов этого assets я использую ранее созданный файл check-pc.js, с помощью хранящейся в нём функции я собираюсь контролировать ширину соответствующего блока в зависимости от ширины окна браузера, точно так же, как я делал это на стартовой странице.

Изображение, адрес которого используется в блоке page_body только что отредактированного шаблона необходимо создать и скопировать в соответствующий каталог в структуре каталогов приложения.

S8qTI4vwPN.png

В окне текстового редактора сохраняю изменения всех отредактированных только что файлов, в Vim это можно сделать с помощью команды :wa.

Если прямо сейчас переместиться в окно браузера и обновить отображающуюся в нём страницу на адресе http://localhost:5000/babam, то отладочный сервер ответит статус кодом 500 — фатальная ошибка сервера. Снимок экрана не прилагаю, можете проверить самостоятельно. Такие ошибки сервера следует научиться отлаживать, а для этого необходимо уметь читать сообщения отладчика Starlette. Причина данной ошибки в том, что использованные в соответствующих assets шаблона файлы error.css и error.js до сих пор в структуре каталогов приложения не существуют.

В окне текстового редактора создаю файл с именем error.css следующей командой.

:split webapp/static/css/error.css

В этот файл помещаю следующий код.

#mc {
  padding: 8px;
  border-radius: 4px;
  border: 1px solid lemonchiffon;
  background-image: linear-gradient(to bottom, ivory, seashell);
  box-shadow: 0 0 6px salmon;
  color: darkred;
}

h1 {
  margin: 0;
}

p {
  font-style: italic;
  margin: 0;
}

Файл error.js будет храниться в обособленном каталоге с именем подпрограммы, в которой он используется — errors. Создаю сначала этот каталог.

:!mkdir webapp/static/js/errors

И уже в нём создаю новый файл.

:edit webapp/static/js/errors/error.js

В этом файле пишу следующий код.

$(function() {
  "use strict";
  checkPC(860);
});

Код файла error.js выглядит совершенно аналогично коду файла index.js с той лишь разницей, что в нём меньше процедур. В рамках этого сценария мне необходимо контролировать ширину блока page-content на странице в зависимости от ширины окна браузера, что я и делю с помощью разработанной ранее функции checkPC. Вызов этой функции я помещаю внутри функции объекта Jquery.

Структура каталогов приложения

Давайте ещё раз посмотрим на полученную в итоге структуру каталогов приложения, в ней нужно обратить внимание на одну характерную особенность.

Hp7wxtkAtA.png

Перечень хранящихся в базовом каталоге приложения подпрограмм совпадает с аналогичным перечнем вложенных каталогов в каталоге js. Аналогичное сходство можно обнаружить и в каталоге templates.

c9ZBtkl0lb.png

Выбранный порядок хранения файлов качественно облегчает жизнь разработчика впоследствии, и его я буду придерживаться впредь, на последующих этапах разработки этого web-приложения.

Тестируем errors в браузере

В окне текстового редактора ещё раз сохраняю изменения всех только что отредактированных файлов. Перемещаюсь в окно браузера и в очередной раз обновляю страницу на адресе http://localhost:5000/babam. И вот что показывает мой браузер на этот раз.

pKYSiOzVhj.png

Вот как выглядит это же окно браузера без вкладки инструментов разработчика.

PrHiubWj27.png

Как видно на снимках экрана выше, статус код полученной страницы всё ещё 404, но теперь страница имеет разметку, оформление и заданное сценарием JavaScript поведение. Если сравнивать с первым в этом обзоре снимком экрана, можно найти следующие разительные отличия:

  1. У страницы появился заголовок во вкладке браузера;

  2. На странице появилось главное меню и в нём разделы с ключевыми ссылками приложения;

  3. На странице появилось чуть более понятное информационное сообщение для конечного пользователя на языке, которым пользуется для общения с миром целевая группа пользователей сайта;

  4. На странице появился "подвал" с копирайтом и ссылкой на стартовую страницу.

В этом варианте страницы у сайта появится шанс привлечь внимание заблудившегося, случайно оказавшегося на странице 404 странника сети и задержать его в своих пределах как можно дольше. Последующие обстоятельства уже будут зависеть от степени любознательности конечного пользователя и соответствия содержания сайта его интересам и предпочтениям, но с этим всё, увы, гораздо сложней.

Подводим промежуточный итог

Все цели очередного этапа разработки web-приложения достигнуты. В приложении появилась более или менее информативная для конечного пользователя страница с отображением HTTP ошибок со статус кодами 403, 404 и 405. На этой странице присутствуют все элементы используемого макета, которые содержат ссылки на ключевые страницы сервиса. Код приложения в текущей версии можно увидеть в моём профиле на github.com по этой короткой ссылке. А в ближайшем выпуске этого цикла статей я займусь подключением в приложение защищённой сессии, продолжение следует...

Напоминаю, что продолжение этого цикла статей возможно только при наличии в блоге активности. Ваши посещения, подписки, лайки, комментарии и донаты имеют большое значение. Не оставайтесь в стороне, будет интересно...