2012-03-09 04:45:27 +00:00
|
|
|
|
#include <iomanip>
|
2016-02-12 00:56:15 +00:00
|
|
|
|
#include <Poco/InflatingStream.h>
|
2012-03-09 04:45:27 +00:00
|
|
|
|
|
2013-08-10 07:46:45 +00:00
|
|
|
|
#include <Poco/Net/HTTPBasicCredentials.h>
|
2012-03-09 03:06:09 +00:00
|
|
|
|
|
2015-10-05 00:44:40 +00:00
|
|
|
|
#include <DB/Common/Stopwatch.h>
|
2012-03-09 04:45:27 +00:00
|
|
|
|
|
2012-03-09 03:06:09 +00:00
|
|
|
|
#include <DB/IO/ReadBufferFromIStream.h>
|
2012-03-09 06:36:29 +00:00
|
|
|
|
#include <DB/IO/ReadBufferFromString.h>
|
|
|
|
|
#include <DB/IO/ConcatReadBuffer.h>
|
2012-03-09 15:46:52 +00:00
|
|
|
|
#include <DB/IO/CompressedReadBuffer.h>
|
|
|
|
|
#include <DB/IO/CompressedWriteBuffer.h>
|
2012-03-09 03:06:09 +00:00
|
|
|
|
#include <DB/IO/WriteBufferFromString.h>
|
|
|
|
|
#include <DB/IO/WriteHelpers.h>
|
|
|
|
|
|
2012-03-09 04:45:27 +00:00
|
|
|
|
#include <DB/DataStreams/IProfilingBlockInputStream.h>
|
|
|
|
|
|
2012-03-09 03:06:09 +00:00
|
|
|
|
#include <DB/Interpreters/executeQuery.h>
|
2015-05-29 21:37:17 +00:00
|
|
|
|
#include <DB/Interpreters/Quota.h>
|
2012-03-09 03:06:09 +00:00
|
|
|
|
|
2014-04-08 13:43:20 +00:00
|
|
|
|
#include <DB/Common/ExternalTable.h>
|
|
|
|
|
|
2012-03-09 15:46:52 +00:00
|
|
|
|
#include "HTTPHandler.h"
|
|
|
|
|
|
2012-03-09 03:06:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace DB
|
|
|
|
|
{
|
|
|
|
|
|
2016-01-11 21:46:36 +00:00
|
|
|
|
namespace ErrorCodes
|
|
|
|
|
{
|
|
|
|
|
extern const int READONLY;
|
2016-02-12 03:32:05 +00:00
|
|
|
|
extern const int UNKNOWN_COMPRESSION_METHOD;
|
2016-01-11 21:46:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2014-08-04 19:48:50 +00:00
|
|
|
|
void HTTPHandler::processQuery(Poco::Net::HTTPServerRequest & request, Poco::Net::HTTPServerResponse & response, Output & used_output)
|
2012-03-09 03:06:09 +00:00
|
|
|
|
{
|
2013-08-10 07:46:45 +00:00
|
|
|
|
LOG_TRACE(log, "Request URI: " << request.getURI());
|
|
|
|
|
|
|
|
|
|
HTMLForm params(request);
|
|
|
|
|
std::istream & istr = request.stream();
|
2016-02-12 00:56:15 +00:00
|
|
|
|
|
2012-03-09 03:06:09 +00:00
|
|
|
|
BlockInputStreamPtr query_plan;
|
2013-08-10 07:46:45 +00:00
|
|
|
|
|
2013-02-16 13:48:25 +00:00
|
|
|
|
/** Часть запроса может быть передана в параметре query, а часть - POST-ом
|
|
|
|
|
* (точнее - в теле запроса, а метод не обязательно должен быть POST).
|
2012-03-09 06:36:29 +00:00
|
|
|
|
* В таком случае, считается, что запрос - параметр query, затем перевод строки, а затем - данные POST-а.
|
|
|
|
|
*/
|
|
|
|
|
std::string query_param = params.get("query", "");
|
|
|
|
|
if (!query_param.empty())
|
|
|
|
|
query_param += '\n';
|
2014-07-16 01:13:07 +00:00
|
|
|
|
|
2016-02-11 21:40:51 +00:00
|
|
|
|
/** Клиент может указать поддерживаемый метод сжатия (gzip или deflate) в HTTP-заголовке.
|
|
|
|
|
*/
|
2016-02-12 00:56:15 +00:00
|
|
|
|
String http_response_compression_methods = request.get("Accept-Encoding", "");
|
2016-02-19 19:02:20 +00:00
|
|
|
|
bool client_supports_http_compression = false;
|
2016-02-12 00:56:15 +00:00
|
|
|
|
Poco::DeflatingStreamBuf::StreamType http_response_compression_method {};
|
2016-02-11 21:40:51 +00:00
|
|
|
|
|
2016-02-12 00:56:15 +00:00
|
|
|
|
if (!http_response_compression_methods.empty())
|
2016-02-11 21:40:51 +00:00
|
|
|
|
{
|
|
|
|
|
/// Мы поддерживаем gzip или deflate. Если клиент поддерживает оба, то предпочитается gzip.
|
|
|
|
|
/// NOTE Парсинг списка методов слегка некорректный.
|
|
|
|
|
|
2016-02-12 00:56:15 +00:00
|
|
|
|
if (std::string::npos != http_response_compression_methods.find("gzip"))
|
2016-02-11 21:40:51 +00:00
|
|
|
|
{
|
2016-02-19 19:02:20 +00:00
|
|
|
|
client_supports_http_compression = true;
|
2016-02-12 00:56:15 +00:00
|
|
|
|
http_response_compression_method = Poco::DeflatingStreamBuf::STREAM_GZIP;
|
2016-02-11 21:40:51 +00:00
|
|
|
|
}
|
2016-02-12 00:56:15 +00:00
|
|
|
|
else if (std::string::npos != http_response_compression_methods.find("deflate"))
|
2016-02-11 21:40:51 +00:00
|
|
|
|
{
|
2016-02-19 19:02:20 +00:00
|
|
|
|
client_supports_http_compression = true;
|
2016-02-12 00:56:15 +00:00
|
|
|
|
http_response_compression_method = Poco::DeflatingStreamBuf::STREAM_ZLIB;
|
2016-02-11 21:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-19 19:02:20 +00:00
|
|
|
|
used_output.out = new WriteBufferFromHTTPServerResponse(response, client_supports_http_compression, http_response_compression_method);
|
2016-02-11 21:40:51 +00:00
|
|
|
|
|
|
|
|
|
/** Клиент может указать compress в query string.
|
|
|
|
|
* В этом случае, результат сжимается несовместимым алгоритмом для внутреннего использования и этот факт не отражается в HTTP заголовках.
|
|
|
|
|
*/
|
2013-06-21 21:05:16 +00:00
|
|
|
|
if (parse<bool>(params.get("compress", "0")))
|
2014-08-04 19:48:50 +00:00
|
|
|
|
used_output.out_maybe_compressed = new CompressedWriteBuffer(*used_output.out);
|
2012-03-09 15:46:52 +00:00
|
|
|
|
else
|
2014-08-04 19:48:50 +00:00
|
|
|
|
used_output.out_maybe_compressed = used_output.out;
|
2013-08-10 07:46:45 +00:00
|
|
|
|
|
|
|
|
|
/// Имя пользователя и пароль могут быть заданы как в параметрах URL, так и с помощью HTTP Basic authentification (и то, и другое не секъюрно).
|
|
|
|
|
std::string user = params.get("user", "default");
|
|
|
|
|
std::string password = params.get("password", "");
|
|
|
|
|
|
|
|
|
|
if (request.hasCredentials())
|
|
|
|
|
{
|
|
|
|
|
Poco::Net::HTTPBasicCredentials credentials(request);
|
|
|
|
|
|
|
|
|
|
user = credentials.getUsername();
|
|
|
|
|
password = credentials.getPassword();
|
|
|
|
|
}
|
2013-08-12 00:36:18 +00:00
|
|
|
|
|
|
|
|
|
std::string quota_key = params.get("quota_key", "");
|
2014-05-16 01:25:20 +00:00
|
|
|
|
std::string query_id = params.get("query_id", "");
|
2014-07-16 01:13:07 +00:00
|
|
|
|
|
2013-09-14 05:14:22 +00:00
|
|
|
|
Context context = *server.global_context;
|
|
|
|
|
context.setGlobalContext(*server.global_context);
|
2012-03-09 03:06:09 +00:00
|
|
|
|
|
2013-08-12 00:36:18 +00:00
|
|
|
|
context.setUser(user, password, request.clientAddress().host(), quota_key);
|
2014-02-12 17:31:02 +00:00
|
|
|
|
context.setCurrentQueryId(query_id);
|
2013-08-10 07:46:45 +00:00
|
|
|
|
|
2014-04-08 13:43:20 +00:00
|
|
|
|
SharedPtr<ReadBuffer> in_param = new ReadBufferFromString(query_param);
|
2016-02-12 00:56:15 +00:00
|
|
|
|
|
|
|
|
|
/// Данные POST-а могут быть сжаты алгоритмом, указанным в Content-Encoding заголовке.
|
|
|
|
|
String http_request_compression_method_str = request.get("Content-Encoding", "");
|
|
|
|
|
bool http_request_decompress = false;
|
|
|
|
|
Poco::InflatingStreamBuf::StreamType http_request_compression_method {};
|
|
|
|
|
|
|
|
|
|
if (!http_request_compression_method_str.empty())
|
|
|
|
|
{
|
|
|
|
|
if (http_request_compression_method_str == "gzip")
|
|
|
|
|
{
|
|
|
|
|
http_request_decompress = true;
|
|
|
|
|
http_request_compression_method = Poco::InflatingStreamBuf::STREAM_GZIP;
|
|
|
|
|
}
|
|
|
|
|
else if (http_request_compression_method_str == "deflate")
|
|
|
|
|
{
|
|
|
|
|
http_request_decompress = true;
|
|
|
|
|
http_request_compression_method = Poco::InflatingStreamBuf::STREAM_ZLIB;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
throw Exception("Unknown Content-Encoding of HTTP request: " + http_request_compression_method_str,
|
|
|
|
|
ErrorCodes::UNKNOWN_COMPRESSION_METHOD);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::experimental::optional<Poco::InflatingInputStream> decompressing_stream;
|
|
|
|
|
SharedPtr<ReadBuffer> in_post;
|
|
|
|
|
|
|
|
|
|
if (http_request_decompress)
|
|
|
|
|
{
|
|
|
|
|
decompressing_stream.emplace(istr, http_request_compression_method);
|
|
|
|
|
in_post = new ReadBufferFromIStream(decompressing_stream.value());
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
in_post = new ReadBufferFromIStream(istr);
|
|
|
|
|
|
|
|
|
|
/// Также данные могут быть сжаты несовместимым алгоритмом для внутреннего использования - это определяется параметром query_string.
|
2014-04-08 13:43:20 +00:00
|
|
|
|
SharedPtr<ReadBuffer> in_post_maybe_compressed;
|
2016-04-09 23:24:38 +00:00
|
|
|
|
bool in_post_compressed = false;
|
2014-04-08 13:43:20 +00:00
|
|
|
|
|
|
|
|
|
if (parse<bool>(params.get("decompress", "0")))
|
2016-04-09 23:24:38 +00:00
|
|
|
|
{
|
2014-04-08 13:43:20 +00:00
|
|
|
|
in_post_maybe_compressed = new CompressedReadBuffer(*in_post);
|
2016-04-09 23:24:38 +00:00
|
|
|
|
in_post_compressed = true;
|
|
|
|
|
}
|
2014-04-08 13:43:20 +00:00
|
|
|
|
else
|
|
|
|
|
in_post_maybe_compressed = in_post;
|
|
|
|
|
|
|
|
|
|
SharedPtr<ReadBuffer> in;
|
2014-04-24 18:54:36 +00:00
|
|
|
|
|
2016-02-12 00:56:15 +00:00
|
|
|
|
/// Поддержка "внешних данных для обработки запроса".
|
2014-04-24 18:54:36 +00:00
|
|
|
|
if (0 == strncmp(request.getContentType().data(), "multipart/form-data", strlen("multipart/form-data")))
|
2014-04-08 13:43:20 +00:00
|
|
|
|
{
|
|
|
|
|
in = in_param;
|
|
|
|
|
ExternalTablesHandler handler(context, params);
|
|
|
|
|
|
|
|
|
|
params.load(request, istr, handler);
|
|
|
|
|
|
2016-02-12 00:56:15 +00:00
|
|
|
|
/// Удаляем уже нененужные параметры из хранилища, чтобы впоследствии не перепутать их с наcтройками контекста и параметрами запроса.
|
2014-04-08 13:43:20 +00:00
|
|
|
|
for (const auto & it : handler.names)
|
|
|
|
|
{
|
2014-04-09 13:32:00 +00:00
|
|
|
|
params.erase(it + "_format");
|
|
|
|
|
params.erase(it + "_types");
|
|
|
|
|
params.erase(it + "_structure");
|
2014-04-08 13:43:20 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
in = new ConcatReadBuffer(*in_param, *in_post_maybe_compressed);
|
|
|
|
|
|
2016-02-12 03:32:05 +00:00
|
|
|
|
/** Настройки могут быть переопределены в запросе.
|
|
|
|
|
* Некоторые параметры (database, default_format, и все что использовались выше),
|
|
|
|
|
* не относятся к обычным настройкам (Settings).
|
|
|
|
|
*
|
|
|
|
|
* Среди настроек есть также readonly.
|
|
|
|
|
* readonly = 0 - можно выполнять любые запросы и изменять любые настройки
|
|
|
|
|
* readonly = 1 - можно выполнять только запросы на чтение, нельзя изменять настройки
|
|
|
|
|
* readonly = 2 - можно выполнять только запросы на чтение, можно изменять настройки кроме настройки readonly
|
|
|
|
|
*
|
|
|
|
|
* Заметим, что в запросе, если до этого readonly было равно 0,
|
|
|
|
|
* пользователь может изменить любые настройки и одновременно выставить readonly в другое значение.
|
|
|
|
|
*/
|
|
|
|
|
auto & limits = context.getSettingsRef().limits;
|
|
|
|
|
|
|
|
|
|
/// Если метод GET, то это эквивалентно настройке readonly, выставленной в ненулевое значение.
|
|
|
|
|
if (request.getMethod() == Poco::Net::HTTPServerRequest::HTTP_GET)
|
|
|
|
|
{
|
|
|
|
|
if (limits.readonly == 0)
|
|
|
|
|
limits.readonly = 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto readonly_before_query = limits.readonly;
|
|
|
|
|
|
2013-02-17 19:54:32 +00:00
|
|
|
|
for (Poco::Net::NameValueCollection::ConstIterator it = params.begin(); it != params.end(); ++it)
|
|
|
|
|
{
|
|
|
|
|
if (it->first == "database")
|
|
|
|
|
{
|
|
|
|
|
context.setCurrentDatabase(it->second);
|
|
|
|
|
}
|
2013-06-29 18:03:57 +00:00
|
|
|
|
else if (it->first == "default_format")
|
|
|
|
|
{
|
|
|
|
|
context.setDefaultFormat(it->second);
|
|
|
|
|
}
|
2013-02-17 19:54:32 +00:00
|
|
|
|
else if (it->first == "query"
|
|
|
|
|
|| it->first == "compress"
|
2013-08-10 07:46:45 +00:00
|
|
|
|
|| it->first == "decompress"
|
|
|
|
|
|| it->first == "user"
|
2014-05-16 01:25:20 +00:00
|
|
|
|
|| it->first == "password"
|
|
|
|
|
|| it->first == "quota_key"
|
|
|
|
|
|| it->first == "query_id")
|
2013-02-17 19:54:32 +00:00
|
|
|
|
{
|
|
|
|
|
}
|
2016-02-12 03:32:05 +00:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
/// Все остальные параметры запроса рассматриваются, как настройки.
|
|
|
|
|
|
|
|
|
|
if (readonly_before_query == 1)
|
|
|
|
|
throw Exception("Cannot override setting (" + it->first + ") in readonly mode", ErrorCodes::READONLY);
|
|
|
|
|
|
|
|
|
|
if (readonly_before_query && it->first == "readonly")
|
|
|
|
|
throw Exception("Setting 'readonly' cannot be overrided in readonly mode", ErrorCodes::READONLY);
|
|
|
|
|
|
2014-02-13 07:17:22 +00:00
|
|
|
|
context.setSetting(it->first, it->second);
|
2016-02-12 03:32:05 +00:00
|
|
|
|
}
|
2013-02-17 19:54:32 +00:00
|
|
|
|
}
|
2012-08-02 17:33:31 +00:00
|
|
|
|
|
2016-02-19 19:02:20 +00:00
|
|
|
|
/// Сжатие ответа (Content-Encoding) включается только если клиент сказал, что он это понимает (Accept-Encoding)
|
|
|
|
|
/// и выставлена настройка, разрешающая сжатие.
|
|
|
|
|
used_output.out->setCompression(client_supports_http_compression && context.getSettingsRef().enable_http_compression);
|
|
|
|
|
if (client_supports_http_compression)
|
2016-02-12 02:26:04 +00:00
|
|
|
|
used_output.out->setCompressionLevel(context.getSettingsRef().http_zlib_compression_level);
|
|
|
|
|
|
2016-04-09 23:24:38 +00:00
|
|
|
|
/// Возможно, что выставлена настройка - не проверять чексуммы при разжатии данных от клиента, сжатых родным форматом.
|
|
|
|
|
if (in_post_compressed && context.getSettingsRef().http_native_compression_disable_checksumming_on_decompress)
|
|
|
|
|
static_cast<CompressedReadBuffer &>(*in_post_maybe_compressed).disableChecksumming();
|
|
|
|
|
|
2015-06-26 20:48:10 +00:00
|
|
|
|
context.setInterface(Context::Interface::HTTP);
|
|
|
|
|
|
|
|
|
|
Context::HTTPMethod http_method = Context::HTTPMethod::UNKNOWN;
|
|
|
|
|
if (request.getMethod() == Poco::Net::HTTPServerRequest::HTTP_GET)
|
|
|
|
|
http_method = Context::HTTPMethod::GET;
|
|
|
|
|
else if (request.getMethod() == Poco::Net::HTTPServerRequest::HTTP_POST)
|
|
|
|
|
http_method = Context::HTTPMethod::POST;
|
|
|
|
|
|
|
|
|
|
context.setHTTPMethod(http_method);
|
|
|
|
|
|
2015-10-30 21:19:54 +00:00
|
|
|
|
executeQuery(*in, *used_output.out_maybe_compressed, context, query_plan,
|
|
|
|
|
[&response] (const String & content_type) { response.setContentType(content_type); });
|
2012-03-09 03:06:09 +00:00
|
|
|
|
|
2013-12-17 19:45:18 +00:00
|
|
|
|
/// Если не было эксепшена и данные ещё не отправлены - отправляются HTTP заголовки с кодом 200.
|
2014-08-04 19:48:50 +00:00
|
|
|
|
used_output.out->finalize();
|
2012-03-09 03:06:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2015-06-29 21:35:35 +00:00
|
|
|
|
void HTTPHandler::trySendExceptionToClient(const std::string & s,
|
2014-08-04 19:48:50 +00:00
|
|
|
|
Poco::Net::HTTPServerRequest & request, Poco::Net::HTTPServerResponse & response,
|
|
|
|
|
Output & used_output)
|
2012-03-09 03:06:09 +00:00
|
|
|
|
{
|
2014-07-14 19:46:06 +00:00
|
|
|
|
try
|
2012-06-25 05:07:34 +00:00
|
|
|
|
{
|
2014-07-16 01:13:07 +00:00
|
|
|
|
/** Если POST и Keep-Alive, прочитаем тело до конца.
|
|
|
|
|
* Иначе вместо следующего запроса, будет прочитан кусок этого тела.
|
|
|
|
|
*/
|
|
|
|
|
if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_POST
|
|
|
|
|
&& response.getKeepAlive()
|
|
|
|
|
&& !request.stream().eof())
|
|
|
|
|
{
|
|
|
|
|
request.stream().ignore(std::numeric_limits<std::streamsize>::max());
|
|
|
|
|
}
|
|
|
|
|
|
2014-07-14 19:46:06 +00:00
|
|
|
|
response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR);
|
2014-08-04 19:48:50 +00:00
|
|
|
|
|
|
|
|
|
if (!response.sent() && !used_output.out_maybe_compressed)
|
|
|
|
|
{
|
|
|
|
|
/// Ещё ничего не отправляли, и даже не знаем, нужно ли сжимать ответ.
|
2015-06-29 21:35:35 +00:00
|
|
|
|
response.send() << s << std::endl;
|
2014-08-04 19:48:50 +00:00
|
|
|
|
}
|
|
|
|
|
else if (used_output.out_maybe_compressed)
|
|
|
|
|
{
|
|
|
|
|
/** Отправим в использованный (возможно сжатый) поток сообщение об ошибке.
|
|
|
|
|
* Сообщение об ошибке может идти невпопад - после каких-то данных.
|
|
|
|
|
* Также стоит иметь ввиду, что мы могли уже отправить код 200.
|
|
|
|
|
*/
|
|
|
|
|
|
2014-08-04 20:16:49 +00:00
|
|
|
|
/** Если данные есть в буфере, но их ещё не отправили, то и не будем отправлять */
|
|
|
|
|
if (used_output.out->count() - used_output.out->offset() == 0)
|
|
|
|
|
{
|
|
|
|
|
used_output.out_maybe_compressed->position() = used_output.out_maybe_compressed->buffer().begin();
|
|
|
|
|
used_output.out->position() = used_output.out->buffer().begin();
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-29 21:35:35 +00:00
|
|
|
|
writeString(s, *used_output.out_maybe_compressed);
|
2014-08-04 19:48:50 +00:00
|
|
|
|
writeChar('\n', *used_output.out_maybe_compressed);
|
|
|
|
|
used_output.out_maybe_compressed->next();
|
|
|
|
|
used_output.out->finalize();
|
|
|
|
|
}
|
2012-06-25 05:07:34 +00:00
|
|
|
|
}
|
2014-07-14 19:46:06 +00:00
|
|
|
|
catch (...)
|
|
|
|
|
{
|
|
|
|
|
LOG_ERROR(log, "Cannot send exception to client");
|
|
|
|
|
}
|
|
|
|
|
}
|
2012-06-25 05:07:34 +00:00
|
|
|
|
|
2013-08-13 18:59:51 +00:00
|
|
|
|
|
2014-07-14 19:46:06 +00:00
|
|
|
|
void HTTPHandler::handleRequest(Poco::Net::HTTPServerRequest & request, Poco::Net::HTTPServerResponse & response)
|
|
|
|
|
{
|
2014-08-04 19:48:50 +00:00
|
|
|
|
Output used_output;
|
|
|
|
|
|
2012-03-09 03:06:09 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
2015-10-30 21:19:54 +00:00
|
|
|
|
response.setContentType("text/plain; charset=UTF-8");
|
2014-07-14 19:46:06 +00:00
|
|
|
|
|
|
|
|
|
/// Для того, чтобы работал keep-alive.
|
|
|
|
|
if (request.getVersion() == Poco::Net::HTTPServerRequest::HTTP_1_1)
|
|
|
|
|
response.setChunkedTransferEncoding(true);
|
|
|
|
|
|
2014-08-04 19:48:50 +00:00
|
|
|
|
processQuery(request, response, used_output);
|
2012-03-09 03:06:09 +00:00
|
|
|
|
LOG_INFO(log, "Done processing query");
|
|
|
|
|
}
|
|
|
|
|
catch (...)
|
|
|
|
|
{
|
2015-07-01 05:24:08 +00:00
|
|
|
|
tryLogCurrentException(log);
|
2015-06-29 21:35:35 +00:00
|
|
|
|
trySendExceptionToClient(getCurrentExceptionMessage(true), request, response, used_output);
|
2012-03-09 03:06:09 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|