Skip to content

Commit c18f210

Browse files
author
Peter Thorson
committed
Allow deferring the sending of an HTTP response. references zaphoyd#425
This allows processing of long running http handlers to defer their response until it is ready without blocking the network thread.
1 parent f469b90 commit c18f210

File tree

4 files changed

+193
-23
lines changed

4 files changed

+193
-23
lines changed

changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ HEAD
2121
code that was used as a guide for this implementation. Fixes #324
2222
- Feature: Adds a vectored/scatter-gather write handler to the iostream
2323
transport.
24+
- Feature: Adds the ability to defer sending an HTTP response until sometime
25+
after the `http_handler` is run. This allows processing of long running http
26+
handlers to defer their response until it is ready without blocking the
27+
network thread. references #425
2428
- Improvement: `echo_server_tls` has been update to demonstrate how to configure
2529
it for Mozilla's recommended intermediate and modern TLS security profiles.
2630
- Improvement: `endpoint::set_timer` now uses a steady clock provided by

test/connection/connection.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ void http_func(server* s, websocketpp::connection_hdl hdl) {
167167
con->set_status(websocketpp::http::status_code::ok);
168168
}
169169

170+
void defer_http_func(server* s, bool * deferred, websocketpp::connection_hdl hdl) {
171+
*deferred = true;
172+
173+
server::connection_ptr con = s->get_con_from_hdl(hdl);
174+
175+
websocketpp::lib::error_code ec;
176+
con->defer_http_response(ec);
177+
}
178+
170179
void check_on_fail(server* s, websocketpp::lib::error_code ec, bool & called,
171180
websocketpp::connection_hdl hdl)
172181
{
@@ -223,6 +232,44 @@ BOOST_AUTO_TEST_CASE( http_request ) {
223232
BOOST_CHECK_EQUAL(run_server_test(s,input), output);
224233
}
225234

235+
BOOST_AUTO_TEST_CASE( deferred_http_request ) {
236+
std::string input = "GET /foo/bar HTTP/1.1\r\nHost: www.example.com\r\nOrigin: http://www.example.com\r\n\r\n";
237+
std::string output = "HTTP/1.1 200 OK\r\nContent-Length: 8\r\nServer: ";
238+
output+=websocketpp::user_agent;
239+
output+="\r\n\r\n/foo/bar";
240+
241+
server s;
242+
server::connection_ptr con;
243+
bool deferred = false;
244+
s.set_http_handler(bind(&defer_http_func,&s, &deferred,::_1));
245+
246+
s.clear_access_channels(websocketpp::log::alevel::all);
247+
s.clear_error_channels(websocketpp::log::elevel::all);
248+
249+
std::stringstream ostream;
250+
s.register_ostream(&ostream);
251+
252+
con = s.get_connection();
253+
con->start();
254+
255+
BOOST_CHECK(!deferred);
256+
BOOST_CHECK_EQUAL(ostream.str(), "");
257+
con->read_some(input.data(),input.size());
258+
BOOST_CHECK(deferred);
259+
BOOST_CHECK_EQUAL(ostream.str(), "");
260+
261+
con->set_body(con->get_resource());
262+
con->set_status(websocketpp::http::status_code::ok);
263+
264+
websocketpp::lib::error_code ec;
265+
con->send_http_response(ec);
266+
BOOST_CHECK_EQUAL(ec, websocketpp::lib::error_code());
267+
BOOST_CHECK_EQUAL(ostream.str(), output);
268+
con->send_http_response(ec);
269+
BOOST_CHECK_EQUAL(ec, make_error_code(websocketpp::error::invalid_state));
270+
271+
}
272+
226273
BOOST_AUTO_TEST_CASE( request_no_server_header ) {
227274
std::string input = "GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nOrigin: http://www.example.com\r\n\r\n";
228275
std::string output = "HTTP/1.1 101 Switching Protocols\r\nConnection: upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\nUpgrade: websocket\r\n\r\n";

websocketpp/connection.hpp

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,20 @@ namespace internal_state {
214214
PROCESS_CONNECTION = 7
215215
};
216216
} // namespace internal_state
217+
218+
219+
namespace http_state {
220+
// states to keep track of the progress of http connections
221+
222+
enum value {
223+
init = 0,
224+
deferred = 1,
225+
headers_written = 2,
226+
body_written = 3,
227+
closed = 4
228+
};
229+
} // namespace http_state
230+
217231
} // namespace session
218232

219233
/// Represents an individual WebSocket connection
@@ -312,6 +326,7 @@ class connection
312326
, m_local_close_code(close::status::abnormal_close)
313327
, m_remote_close_code(close::status::abnormal_close)
314328
, m_is_http(false)
329+
, m_http_state(session::http_state::init)
315330
, m_was_clean(false)
316331
{
317332
m_alog.write(log::alevel::devel,"connection constructor");
@@ -1060,6 +1075,49 @@ class connection
10601075
request_type const & get_request() const {
10611076
return m_request;
10621077
}
1078+
1079+
/// Defer HTTP Response until later
1080+
/**
1081+
* Used in the http handler to defer the HTTP response for this connection
1082+
* until later. Handshake timers will be canceled and the connection will be
1083+
* left open until `send_http_response` or an equivalent is called.
1084+
*
1085+
* Warning: deferred connections won't time out and as a result can tie up
1086+
* resources.
1087+
*
1088+
* @since 0.6.0
1089+
*
1090+
* @param ec A status code, zero on success, non-zero otherwise
1091+
*/
1092+
void defer_http_response(lib::error_code & ec);
1093+
1094+
/// Send deferred HTTP Response
1095+
/**
1096+
* Sends an http response to an HTTP connection that was deferred. This will
1097+
* send a complete response including all headers, status line, and body
1098+
* text. The connection will be closed afterwards.
1099+
*
1100+
* @since 0.6.0
1101+
*
1102+
* @param ec A status code, zero on success, non-zero otherwise
1103+
*/
1104+
void send_http_response(lib::error_code & ec);
1105+
1106+
// TODO HTTPNBIO: write_headers
1107+
// function that processes headers + status so far and writes it to the wire
1108+
// beginning the HTTP response body state. This method will ignore anything
1109+
// in the response body.
1110+
1111+
// TODO HTTPNBIO: write_body_message
1112+
// queues the specified message_buffer for async writing
1113+
1114+
// TODO HTTPNBIO: finish connection
1115+
//
1116+
1117+
// TODO HTTPNBIO: write_response
1118+
// Writes the whole response, headers + body and closes the connection
1119+
1120+
10631121

10641122
/////////////////////////////////////////////////////////////
10651123
// Pass-through access to the other connection information //
@@ -1202,7 +1260,8 @@ class connection
12021260
void handle_read_http_response(lib::error_code const & ec,
12031261
size_t bytes_transferred);
12041262

1205-
void handle_send_http_response(lib::error_code const & ec);
1263+
1264+
void handle_write_http_response(lib::error_code const & ec);
12061265
void handle_send_http_request(lib::error_code const & ec);
12071266

12081267
void handle_open_handshake_timeout(lib::error_code const & ec);
@@ -1254,13 +1313,13 @@ class connection
12541313
lib::error_code process_handshake_request();
12551314
private:
12561315
/// Completes m_response, serializes it, and sends it out on the wire.
1257-
void send_http_response(lib::error_code const & ec);
1316+
void write_http_response(lib::error_code const & ec);
12581317

12591318
/// Sends an opening WebSocket connect request
12601319
void send_http_request();
12611320

1262-
/// Alternate path for send_http_response in error conditions
1263-
void send_http_response_error(lib::error_code const & ec);
1321+
/// Alternate path for write_http_response in error conditions
1322+
void write_http_response_error(lib::error_code const & ec);
12641323

12651324
/// Process control message
12661325
/**
@@ -1510,6 +1569,10 @@ class connection
15101569
/// A flag that gets set once it is determined that the connection is an
15111570
/// HTTP connection and not a WebSocket one.
15121571
bool m_is_http;
1572+
1573+
/// A flag that gets set when the completion of an http connection is
1574+
/// deferred until later.
1575+
session::http_state::value m_http_state;
15131576

15141577
bool m_was_clean;
15151578

websocketpp/impl/connection_impl.hpp

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,57 @@ void connection<config>::remove_header(std::string const & key)
635635
}
636636
}
637637

638+
/// Defer HTTP Response until later
639+
/**
640+
* Used in the http handler to defer the HTTP response for this connection
641+
* until later. Handshake timers will be canceled and the connection will be
642+
* left open until `send_http_response` or an equivalent is called.
643+
*
644+
* Warning: deferred connections won't time out and as a result can tie up
645+
* resources.
646+
*
647+
* @param ec A status code, zero on success, non-zero otherwise
648+
*/
649+
template <typename config>
650+
void connection<config>::defer_http_response(lib::error_code & ec) {
651+
// Cancel handshake timer, otherwise the connection will time out and we'll
652+
// close the connection before the app has a chance to send a response.
653+
if (m_handshake_timer) {
654+
m_handshake_timer->cancel();
655+
m_handshake_timer.reset();
656+
}
657+
658+
// Do something to signal deferral
659+
m_http_state = session::http_state::deferred;
660+
661+
ec = lib::error_code();
662+
}
638663

664+
/// Send deferred HTTP Response
665+
/**
666+
* Sends an http response to an HTTP connection that was deferred. This will
667+
* send a complete response including all headers, status line, and body
668+
* text. The connection will be closed afterwards.
669+
*
670+
* @since 0.6.0
671+
*
672+
* @param ec A status code, zero on success, non-zero otherwise
673+
*/
674+
template <typename config>
675+
void connection<config>::send_http_response(lib::error_code & ec) {
676+
{
677+
scoped_lock_type lock(m_connection_state_lock);
678+
if (m_http_state != session::http_state::deferred) {
679+
ec = error::make_error_code(error::invalid_state);
680+
return;
681+
}
682+
683+
m_http_state = session::http_state::body_written;
684+
}
685+
686+
this->write_http_response(lib::error_code());
687+
ec = lib::error_code();
688+
}
639689

640690

641691

@@ -728,7 +778,7 @@ void connection<config>::read_handshake(size_t num_bytes) {
728778
);
729779
}
730780

731-
// All exit paths for this function need to call send_http_response() or submit
781+
// All exit paths for this function need to call write_http_response() or submit
732782
// a new read request with this function as the handler.
733783
template <typename config>
734784
void connection<config>::handle_read_handshake(lib::error_code const & ec,
@@ -784,7 +834,7 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
784834
// All HTTP exceptions will result in this request failing and an error
785835
// response being returned. No more bytes will be read in this con.
786836
m_response.set_status(e.m_error_code,e.m_error_msg);
787-
this->send_http_response_error(error::make_error_code(error::http_parse_error));
837+
this->write_http_response_error(error::make_error_code(error::http_parse_error));
788838
return;
789839
}
790840

@@ -806,7 +856,7 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
806856
if (m_request.ready()) {
807857
lib::error_code processor_ec = this->initialize_processor();
808858
if (processor_ec) {
809-
this->send_http_response_error(processor_ec);
859+
this->write_http_response_error(processor_ec);
810860
return;
811861
}
812862

@@ -823,7 +873,7 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
823873
// TODO: need more bytes
824874
m_alog.write(log::alevel::devel,"short key3 read");
825875
m_response.set_status(http::status_code::internal_server_error);
826-
this->send_http_response_error(processor::error::make_error_code(processor::error::short_key3));
876+
this->write_http_response_error(processor::error::make_error_code(processor::error::short_key3));
827877
return;
828878
}
829879
}
@@ -847,7 +897,9 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
847897

848898
// We have the complete request. Process it.
849899
lib::error_code handshake_ec = this->process_handshake_request();
850-
this->send_http_response(handshake_ec);
900+
if (!m_is_http || m_http_state != session::http_state::deferred) {
901+
this->write_http_response(handshake_ec);
902+
}
851903
} else {
852904
// read at least 1 more byte
853905
transport_con_type::async_read_at_least(
@@ -864,26 +916,26 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
864916
}
865917
}
866918

867-
// send_http_response requires the request to be fully read and the connection
919+
// write_http_response requires the request to be fully read and the connection
868920
// to be in the PROCESS_HTTP_REQUEST state. In some cases we can detect errors
869921
// before the request is fully read (specifically at a point where we aren't
870922
// sure if the hybi00 key3 bytes need to be read). This method sets the correct
871-
// state and calls send_http_response
923+
// state and calls write_http_response
872924
template <typename config>
873-
void connection<config>::send_http_response_error(lib::error_code const & ec) {
925+
void connection<config>::write_http_response_error(lib::error_code const & ec) {
874926
if (m_internal_state != istate::READ_HTTP_REQUEST) {
875927
m_alog.write(log::alevel::devel,
876-
"send_http_response_error called in invalid state");
928+
"write_http_response_error called in invalid state");
877929
this->terminate(error::make_error_code(error::invalid_state));
878930
return;
879931
}
880932

881933
m_internal_state = istate::PROCESS_HTTP_REQUEST;
882934

883-
this->send_http_response(ec);
935+
this->write_http_response(ec);
884936
}
885937

886-
// All exit paths for this function need to call send_http_response() or submit
938+
// All exit paths for this function need to call write_http_response() or submit
887939
// a new read request with this function as the handler.
888940
template <typename config>
889941
void connection<config>::handle_read_frame(lib::error_code const & ec,
@@ -1113,6 +1165,7 @@ lib::error_code connection<config>::process_handshake_request() {
11131165
if (m_http_handler) {
11141166
m_is_http = true;
11151167
m_http_handler(m_connection_hdl);
1168+
11161169
if (m_state == session::state::closed) {
11171170
return error::make_error_code(error::http_connection_ended);
11181171
}
@@ -1207,8 +1260,8 @@ lib::error_code connection<config>::process_handshake_request() {
12071260
}
12081261

12091262
template <typename config>
1210-
void connection<config>::send_http_response(lib::error_code const & ec) {
1211-
m_alog.write(log::alevel::devel,"connection send_http_response");
1263+
void connection<config>::write_http_response(lib::error_code const & ec) {
1264+
m_alog.write(log::alevel::devel,"connection write_http_response");
12121265

12131266
if (ec == error::make_error_code(error::http_connection_ended)) {
12141267
m_alog.write(log::alevel::http,"An HTTP handler took over the connection.");
@@ -1254,16 +1307,16 @@ void connection<config>::send_http_response(lib::error_code const & ec) {
12541307
m_handshake_buffer.data(),
12551308
m_handshake_buffer.size(),
12561309
lib::bind(
1257-
&type::handle_send_http_response,
1310+
&type::handle_write_http_response,
12581311
type::get_shared(),
12591312
lib::placeholders::_1
12601313
)
12611314
);
12621315
}
12631316

12641317
template <typename config>
1265-
void connection<config>::handle_send_http_response(lib::error_code const & ec) {
1266-
m_alog.write(log::alevel::devel,"handle_send_http_response");
1318+
void connection<config>::handle_write_http_response(lib::error_code const & ec) {
1319+
m_alog.write(log::alevel::devel,"handle_write_http_response");
12671320

12681321
lib::error_code ecm = ec;
12691322

@@ -1279,7 +1332,7 @@ void connection<config>::handle_send_http_response(lib::error_code const & ec) {
12791332
// usually by the handshake timer. This is basically expected
12801333
// (though hopefully rare) and there is nothing we can do so ignore.
12811334
m_alog.write(log::alevel::devel,
1282-
"handle_send_http_response invoked after connection was closed");
1335+
"handle_write_http_response invoked after connection was closed");
12831336
return;
12841337
} else {
12851338
ecm = error::make_error_code(error::invalid_state);
@@ -1294,7 +1347,7 @@ void connection<config>::handle_send_http_response(lib::error_code const & ec) {
12941347
return;
12951348
}
12961349

1297-
log_err(log::elevel::rerror,"handle_send_http_response",ecm);
1350+
log_err(log::elevel::rerror,"handle_write_http_response",ecm);
12981351
this->terminate(ecm);
12991352
return;
13001353
}
@@ -1608,7 +1661,10 @@ void connection<config>::terminate(lib::error_code const & ec) {
16081661
m_local_close_reason = ec.message();
16091662
}
16101663

1611-
// TODO: does this need a mutex?
1664+
// TODO: does any of this need a mutex?
1665+
if (m_is_http) {
1666+
m_http_state = session::http_state::closed;
1667+
}
16121668
if (m_state == session::state::connecting) {
16131669
m_state = session::state::closed;
16141670
tstat = failed;

0 commit comments

Comments
 (0)