Unverified Commit 81b2a4c2 authored by Harris Hancock's avatar Harris Hancock Committed by GitHub

Merge pull request #861 from capnproto/harris/http-error-hook

Add HttpServerErrorHandler interface to provide visibility and customization of protocol errors
parents c661e0d2 24678ff3
......@@ -1283,15 +1283,15 @@ private:
template <typename T>
class Maybe<T&>: public DisallowConstCopyIfNotConst<T> {
public:
Maybe(): ptr(nullptr) {}
Maybe(T& t): ptr(&t) {}
Maybe(T* t): ptr(t) {}
constexpr Maybe(): ptr(nullptr) {}
constexpr Maybe(T& t): ptr(&t) {}
constexpr Maybe(T* t): ptr(t) {}
template <typename U>
inline Maybe(Maybe<U&>& other): ptr(other.ptr) {}
inline constexpr Maybe(Maybe<U&>& other): ptr(other.ptr) {}
template <typename U>
inline Maybe(const Maybe<U&>& other): ptr(const_cast<const U*>(other.ptr)) {}
inline Maybe(decltype(nullptr)): ptr(nullptr) {}
inline constexpr Maybe(const Maybe<U&>& other): ptr(const_cast<const U*>(other.ptr)) {}
inline constexpr Maybe(decltype(nullptr)): ptr(nullptr) {}
inline Maybe& operator=(T& other) { ptr = &other; return *this; }
inline Maybe& operator=(T* other) { ptr = other; return *this; }
......
......@@ -2400,6 +2400,161 @@ KJ_TEST("HttpServer threw exception") {
KJ_EXPECT(text.startsWith("HTTP/1.1 500 Internal Server Error"), text);
}
KJ_TEST("HttpServer bad request") {
KJ_HTTP_TEST_SETUP_IO;
kj::TimerImpl timer(kj::origin<kj::TimePoint>());
auto pipe = KJ_HTTP_TEST_CREATE_2PIPE;
HttpHeaderTable table;
BrokenHttpService service;
HttpServer server(timer, table, service);
auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
static constexpr auto request = "bad request\r\n\r\n"_kj;
auto writePromise = pipe.ends[1]->write(request.begin(), request.size());
auto response = pipe.ends[1]->readAllText().wait(waitScope);
KJ_EXPECT(writePromise.poll(waitScope));
writePromise.wait(waitScope);
static constexpr auto expectedResponse =
"HTTP/1.1 400 Bad Request\r\n"
"Connection: close\r\n"
"Content-Length: 54\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"ERROR: The headers sent by your client were not valid."_kj;
KJ_EXPECT(expectedResponse == response, expectedResponse, response);
}
// Ensure that HttpServerSettings can continue to be constexpr.
KJ_UNUSED static constexpr HttpServerSettings STATIC_CONSTEXPR_SETTINGS {};
class TestErrorHandler: public HttpServerErrorHandler {
public:
kj::Promise<void> handleClientProtocolError(
kj::StringPtr message, kj::HttpService::Response& response) override {
return sendError(400, "Bad Request", kj::str("Saw protocol error: ", message), response);
}
kj::Promise<void> handleApplicationError(
kj::Exception exception, kj::Maybe<kj::HttpService::Response&> response) override {
return sendError(500, "Internal Server Error",
kj::str("Saw application error: ", exception.getDescription()), response);
}
kj::Promise<void> handleNoResponse(kj::HttpService::Response& response) override {
return sendError(500, "Internal Server Error", kj::str("Saw no response."), response);
}
static TestErrorHandler instance;
private:
kj::Promise<void> sendError(uint statusCode, kj::StringPtr statusText, String message,
Maybe<HttpService::Response&> response) {
KJ_IF_MAYBE(r, response) {
HttpHeaderTable headerTable;
HttpHeaders headers(headerTable);
auto body = r->send(statusCode, statusText, headers, message.size());
return body->write(message.begin(), message.size()).attach(kj::mv(body), kj::mv(message));
} else {
KJ_LOG(ERROR, "Saw an error but too late to report to client.");
return kj::READY_NOW;
}
}
};
TestErrorHandler TestErrorHandler::instance {};
KJ_TEST("HttpServer no response, custom error handler") {
auto PIPELINE_TESTS = pipelineTestCases();
KJ_HTTP_TEST_SETUP_IO;
kj::TimerImpl timer(kj::origin<kj::TimePoint>());
auto pipe = KJ_HTTP_TEST_CREATE_2PIPE;
HttpServerSettings settings {};
settings.errorHandler = TestErrorHandler::instance;
HttpHeaderTable table;
BrokenHttpService service;
HttpServer server(timer, table, service, settings);
auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
// Do one request.
pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
.wait(waitScope);
auto text = pipe.ends[1]->readAllText().wait(waitScope);
KJ_EXPECT(text ==
"HTTP/1.1 500 Internal Server Error\r\n"
"Connection: close\r\n"
"Content-Length: 16\r\n"
"\r\n"
"Saw no response.", text);
}
KJ_TEST("HttpServer threw exception, custom error handler") {
auto PIPELINE_TESTS = pipelineTestCases();
KJ_HTTP_TEST_SETUP_IO;
kj::TimerImpl timer(kj::origin<kj::TimePoint>());
auto pipe = KJ_HTTP_TEST_CREATE_2PIPE;
HttpServerSettings settings {};
settings.errorHandler = TestErrorHandler::instance;
HttpHeaderTable table;
BrokenHttpService service(KJ_EXCEPTION(FAILED, "failed"));
HttpServer server(timer, table, service, settings);
auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
// Do one request.
pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
.wait(waitScope);
auto text = pipe.ends[1]->readAllText().wait(waitScope);
KJ_EXPECT(text ==
"HTTP/1.1 500 Internal Server Error\r\n"
"Connection: close\r\n"
"Content-Length: 29\r\n"
"\r\n"
"Saw application error: failed", text);
}
KJ_TEST("HttpServer bad request, custom error handler") {
KJ_HTTP_TEST_SETUP_IO;
kj::TimerImpl timer(kj::origin<kj::TimePoint>());
auto pipe = KJ_HTTP_TEST_CREATE_2PIPE;
HttpServerSettings settings {};
settings.errorHandler = TestErrorHandler::instance;
HttpHeaderTable table;
BrokenHttpService service;
HttpServer server(timer, table, service, settings);
auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
static constexpr auto request = "bad request\r\n\r\n"_kj;
auto writePromise = pipe.ends[1]->write(request.begin(), request.size());
auto response = pipe.ends[1]->readAllText().wait(waitScope);
KJ_EXPECT(writePromise.poll(waitScope));
writePromise.wait(waitScope);
static constexpr auto expectedResponse =
"HTTP/1.1 400 Bad Request\r\n"
"Connection: close\r\n"
"Content-Length: 74\r\n"
"\r\n"
"Saw protocol error: ERROR: The headers sent by your client were not valid."_kj;
KJ_EXPECT(expectedResponse == response, expectedResponse, response);
}
class PartialResponseService final: public HttpService {
// HttpService that sends a partial response then throws.
public:
......
This diff is collapsed.
......@@ -751,6 +751,8 @@ WebSocketPipe newWebSocketPipe();
// end. No buffering occurs -- a message send does not complete until a corresponding receive
// accepts the message.
class HttpServerErrorHandler;
struct HttpServerSettings {
kj::Duration headerTimeout = 15 * kj::SECONDS;
// After initial connection open, or after receiving the first byte of a pipelined request,
......@@ -767,6 +769,44 @@ struct HttpServerSettings {
// request so that it can pipeline the next one. We'll give them a grace period defined by the
// above two values -- if they hit either one, we'll close the socket, but if the request
// completes, we'll let the connection stay open to handle more requests.
kj::Maybe<HttpServerErrorHandler&> errorHandler = nullptr;
// Customize how client protocol errors and service application exceptions are handled by the
// HttpServer. If null, HttpServerErrorHandler's default implementation will be used.
};
class HttpServerErrorHandler {
public:
virtual kj::Promise<void> handleClientProtocolError(
kj::StringPtr message, kj::HttpService::Response& response);
virtual kj::Promise<void> handleApplicationError(
kj::Exception exception, kj::Maybe<kj::HttpService::Response&> response);
virtual kj::Promise<void> handleNoResponse(kj::HttpService::Response& response);
// Override these functions to customize error handling during the request/response cycle.
//
// Client protocol errors arise when the server receives an HTTP message that fails to parse. As
// such, HttpService::request() will not have been called yet, and the handler is always
// guaranteed an opportunity to send a response. The default implementation of
// handleClientProtocolError() replies with a 400 Bad Request response.
//
// Application errors arise when HttpService::request() throws an exception. The default
// implementation of handleApplicationError() maps the following exception types to HTTP statuses,
// and generates bodies from the stringified exceptions:
//
// - OVERLOADED: 503 Service Unavailable
// - UNIMPLEMENTED: 501 Not Implemented
// - DISCONNECTED: (no response)
// - FAILED: 500 Internal Server Error
//
// No-response errors occur when HttpService::request() allows its promise to settle before
// sending a response. The default implementation of handleNoResponse() replies with a 500
// Internal Server Error response.
//
// Unlike `HttpService::request()`, when calling `response.send()` in the context of one of these
// functions, a "Connection: close" header will be added, and the connection will be closed.
//
// Also unlike `HttpService::request()`, it is okay to return kj::READY_NOW without calling
// `response.send()`. In this case, no response will be sent, and the connection will be closed.
};
class HttpServer final: private kj::TaskSet::ErrorHandler {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment