пятница, 29 октября 2010 г.

RESTAS и специальные страницы

Если в обработчике запроса для сервера Hunchentoot выставляется код возврата в какой-либо из кодов ошибок, то Hunchentoot игнорирует возвращаемое диспетчером значение и самостоятельно формирует соответствующую стандартную специальную страницу. Такое поведение не всегда является приемлемым. Изменить его можно с помощью переменной hunchentoot:*handle-http-errors-p* - если она выставлена в NIL, то вся ответственность за формирование специальных страниц ложится на пользовательский код. Однако, изменять её глобально (например, при загрузке системы) тоже не очень хорошо (потому что глобально). На самом деле я бы хотел, что бы пользовательский код смог указать будет ли он сам формировать специальную страницу или же пусть Hunchentoot генерирует стандартную. Самое простое решение - изменять значение переменной hunchentoot:*handle-http-errors-p* в пользовательском коде, но так, что бы эти изменения происходили не глобально, а только в контексте обрабатываемого запроса. Превратить глобальную переменную hunchentoot:*handle-http-errors-p* в "более локальную" очень просто, для RESTAS я сделал это так:
(defmethod hunchentoot:process-request ((request restas-request))
(let ((hunchentoot:*handle-http-errors-p* hunchentoot:*handle-http-errors-p*))
(call-next-method)))
Теперь можно смело менять значение переменной hunchentoot:*handle-http-errors-p* без какого-либо страха что это отразится на функционировании всей системы.

Если обработчик маршрута в RESTAS возвращает целое число, то оно интерпретируется как код возврата, за что отвечает соответствующая специализация метода restas:render-object. Что бы упростить определение для модулей своих способов формирования специальных страниц я переписал эту специализацию следующим образом:
(defmethod render-object :before (designer (code integer))
(setf hunchentoot:*handle-http-errors-p* nil
(hunchentoot:return-code*) code))

(defmethod render-object (designer (code integer))
"Default handler for HTTP status code"
(declare (ignore designer))
(setf hunchentoot:*handle-http-errors-p* t))
Это реализация по-умолчанию, которая в :before методе выставляет код возврата и присваивает переменной hunchentoot:*handle-http-errors-p* значение NIL, а в основном методе присваивает переменной hunchentoot:*handle-http-errors-p* значение T. Таким образом, по-умолчанию при возврате из маршрута целого числа за формирование специальной страницы будет отвечать Hunchentoot. Но, определив свой тип designer (и присвоив объект этого типа переменной модуля *default-render-method*) можно изменить это поведение и формировать специальные страницы в пользовательском коде. Очень удобно, что при этом также можно использовать и eql-специализаторы, например:
(defmethod restas:render-object ((designer mydrawer) (code (eql hunchentoot:+http-internal-server-error+)))
(setf (hunchentoot:content-type*) "text/plain")
"Шеф, всё очень плохо!")
При этом, может случить так, что если маршрут возвращает hunchentoot:+http-not-found+, то будет формироваться "красивое" сообщение о том, что на данном ресурсе такой страницы нет, но если RESTAS просто не найдёт подходящего маршрута, то будет отдаваться стандартная страница Hunchentoot. Для решение данной проблемы проще всего определить "универсальный" маршрут, который будет проверяться в последнюю очередь:
(restas:define-route not-found ("*any")
hunchentoot:+http-not-found+)
Описанный подход (который можно использовать с git-версией RESTAS) хорошо подходит для обработки специальных страниц общим для всего модуля образом. Но сейчас у меня есть один модуль, который предоставляет некоторое API для клиентского кода. Если маршрут в этом модуле отрабатывает успешно, то клиенту возвращаются данные в формате JSON. А в случае ошибки я бы хотел возвращать код возврата hunchentoot:+http-internal-server-error+ и прикреплять к нему сообщение, описывающие тип ошибки. Для упрощения реализации данного функционала я ввёл следующую функцию:
(defun abort-route-handler (obj &key return-code content-type) ..)
Эта функция немедленно прекращает обработку маршрута, а obj передаётся в restas:render-object для формирования ответа. Тонкость в том, что если в данную функцию передан return-code, то значение переменной hunchentoot:*handle-http-errors-p* выставляется в NIL. Теперь, вызов
(restas:abort-route-handler "Очень плохие данные!"
:return-code hunchentoot:+http-internal-server-error+
:content-type "text/plain")
отправит клиенту ответ с кодом 500 и сообщением "Очень плохие данные!".

5 комментариев:

  1. Класс! Возникающие задачи решаются мгновенно без шуму и пыли.

    ОтветитьУдалить
  2. @likfly

    Да давно хотел что-то подобное сделать, но код Hunchentoot в этой части несколько запутан (и, видимо, очень стар), так что я не сразу понял как это нужно делать. Но тут возникла реальная потребность в таком функционале и оказалось, что проблема решается несколько строк кода.

    ОтветитьУдалить
  3. Опечатка в http://restas.lisper.ru/ru/tutorial/pastebin.html#pastebin в последнем предложении в слове "кода".

    ОтветитьУдалить
  4. маленькая опечатка: При этом, может случитьСЯ так, что если маршрут возвращает hunchentoot:+http-not-found+

    ОтветитьУдалить