Skip to content

Commit 0b9ddab

Browse files
tomusdrwTyera Eulberg
authored andcommitted
Handle keep-alive correctly (paritytech#333)
* Proper handling of keep-alive. * Fix windows build again. * Adhere to the HTTP/1.1 spec more (don't return the header as it's implied)
1 parent 0bf5835 commit 0b9ddab

File tree

4 files changed

+120
-1
lines changed

4 files changed

+120
-1
lines changed

http/src/handler.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub struct ServerHandler<M: Metadata = (), S: Middleware<M> = middleware::Noop>
2525
rest_api: RestApi,
2626
health_api: Option<(String, String)>,
2727
max_request_body_size: usize,
28+
keep_alive: bool,
2829
}
2930

3031
impl<M: Metadata, S: Middleware<M>> ServerHandler<M, S> {
@@ -39,6 +40,7 @@ impl<M: Metadata, S: Middleware<M>> ServerHandler<M, S> {
3940
rest_api: RestApi,
4041
health_api: Option<(String, String)>,
4142
max_request_body_size: usize,
43+
keep_alive: bool,
4244
) -> Self {
4345
ServerHandler {
4446
jsonrpc_handler,
@@ -50,6 +52,7 @@ impl<M: Metadata, S: Middleware<M>> ServerHandler<M, S> {
5052
rest_api,
5153
health_api,
5254
max_request_body_size,
55+
keep_alive,
5356
}
5457
}
5558
}
@@ -89,6 +92,7 @@ impl<M: Metadata, S: Middleware<M>> Service for ServerHandler<M, S> {
8992
cors_domains: self.cors_domains.clone(),
9093
cors_headers: self.cors_allowed_headers.clone(),
9194
continue_on_invalid_cors: should_continue_on_invalid_cors,
95+
keep_alive: self.keep_alive,
9296
},
9397
is_options: false,
9498
cors_max_age: self.cors_max_age,
@@ -97,6 +101,8 @@ impl<M: Metadata, S: Middleware<M>> Service for ServerHandler<M, S> {
97101
rest_api: self.rest_api,
98102
health_api: self.health_api.clone(),
99103
max_request_body_size: self.max_request_body_size,
104+
// initial value, overwritten when reading client headers
105+
keep_alive: true,
100106
})
101107
}
102108
}
@@ -159,6 +165,7 @@ enum RpcHandlerState<M, F, G> where
159165
cors_domains: CorsDomains,
160166
cors_headers: cors::AccessControlAllowHeaders,
161167
continue_on_invalid_cors: bool,
168+
keep_alive: bool,
162169
},
163170
ReadingBody {
164171
body: hyper::Body,
@@ -210,6 +217,7 @@ pub struct RpcHandler<M: Metadata, S: Middleware<M>> {
210217
rest_api: RestApi,
211218
health_api: Option<(String, String)>,
212219
max_request_body_size: usize,
220+
keep_alive: bool,
213221
}
214222

215223
impl<M: Metadata, S: Middleware<M>> Future for RpcHandler<M, S> {
@@ -218,10 +226,13 @@ impl<M: Metadata, S: Middleware<M>> Future for RpcHandler<M, S> {
218226

219227
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
220228
let new_state = match mem::replace(&mut self.state, RpcHandlerState::Done) {
221-
RpcHandlerState::ReadingHeaders { request, cors_domains, cors_headers, continue_on_invalid_cors, } => {
229+
RpcHandlerState::ReadingHeaders {
230+
request, cors_domains, cors_headers, continue_on_invalid_cors, keep_alive,
231+
} => {
222232
// Read cors header
223233
self.cors_allow_origin = utils::cors_allow_origin(&request, &cors_domains);
224234
self.cors_allow_headers = utils::cors_allow_headers(&request, &cors_headers);
235+
self.keep_alive = utils::keep_alive(&request, keep_alive);
225236
self.is_options = *request.method() == Method::OPTIONS;
226237
// Read other headers
227238
RpcPollState::Ready(self.read_headers(request, continue_on_invalid_cors))
@@ -288,6 +299,7 @@ impl<M: Metadata, S: Middleware<M>> Future for RpcHandler<M, S> {
288299
self.cors_max_age,
289300
cors_allow_origin.into(),
290301
cors_allow_headers.into(),
302+
self.keep_alive,
291303
);
292304
Ok(Async::Ready(response))
293305
},
@@ -502,6 +514,7 @@ impl<M: Metadata, S: Middleware<M>> RpcHandler<M, S> {
502514
cors_max_age: Option<u32>,
503515
cors_allow_origin: Option<HeaderValue>,
504516
cors_allow_headers: Option<Vec<HeaderValue>>,
517+
keep_alive: bool,
505518
) {
506519
let as_header = |m: Method| m.as_str().parse().expect("`Method` will always parse; qed");
507520
let concat = |headers: &[HeaderValue]| {
@@ -540,6 +553,10 @@ impl<M: Metadata, S: Middleware<M>> RpcHandler<M, S> {
540553
}
541554
}
542555
}
556+
557+
if !keep_alive {
558+
headers.append(header::CONNECTION, HeaderValue::from_static("close"));
559+
}
543560
}
544561

545562
/// Returns true if the `content_type` header indicates a valid JSON

http/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ fn serve<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>>(
517517
rest_api,
518518
health_api.clone(),
519519
max_request_body_size,
520+
keep_alive,
520521
);
521522
tokio::spawn(http.serve_connection(socket, service)
522523
.map_err(|e| error!("Error serving connection: {:?}", e)));

http/src/tests.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,88 @@ fn should_return_error_in_case_of_unsecure_rest_and_no_method() {
11381138
assert_eq!(&response.body, "Supplied content type is not allowed. Content-Type: application/json is required\n");
11391139
}
11401140

1141+
#[test]
1142+
fn should_return_connection_header() {
1143+
// given
1144+
let server = serve(id);
1145+
let addr = server.address().clone();
1146+
1147+
// when
1148+
let req = r#"[{"jsonrpc":"2.0","id":1,"method":"hello"}]"#;
1149+
let response = request(server,
1150+
&format!("\
1151+
POST / HTTP/1.1\r\n\
1152+
Host: localhost:{}\r\n\
1153+
Connection: close\r\n\
1154+
Content-Type: application/json\r\n\
1155+
Content-Length: {}\r\n\
1156+
\r\n\
1157+
{}\r\n\
1158+
", addr.port(), req.as_bytes().len(), req)
1159+
);
1160+
1161+
// then
1162+
assert!(response.headers.contains("connection: close"),
1163+
"Headers missing in {}", response.headers);
1164+
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
1165+
assert_eq!(response.body, world_batch());
1166+
}
1167+
1168+
#[test]
1169+
fn should_close_connection_without_keep_alive() {
1170+
// given
1171+
let server = serve(|builder| builder.keep_alive(false));
1172+
let addr = server.address().clone();
1173+
1174+
// when
1175+
let req = r#"[{"jsonrpc":"2.0","id":1,"method":"hello"}]"#;
1176+
let response = request(server,
1177+
&format!("\
1178+
POST / HTTP/1.1\r\n\
1179+
Host: localhost:{}\r\n\
1180+
Content-Type: application/json\r\n\
1181+
Content-Length: {}\r\n\
1182+
\r\n\
1183+
{}\r\n\
1184+
", addr.port(), req.as_bytes().len(), req)
1185+
);
1186+
1187+
// then
1188+
assert!(response.headers.contains("connection: close"),
1189+
"Header missing in {}", response.headers);
1190+
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
1191+
assert_eq!(response.body, world_batch());
1192+
}
1193+
1194+
#[test]
1195+
fn should_respond_with_close_even_if_client_wants_to_keep_alive() {
1196+
// given
1197+
let server = serve(|builder| builder.keep_alive(false));
1198+
let addr = server.address().clone();
1199+
1200+
// when
1201+
let req = r#"[{"jsonrpc":"2.0","id":1,"method":"hello"}]"#;
1202+
let response = request(server,
1203+
&format!("\
1204+
POST / HTTP/1.1\r\n\
1205+
Host: localhost:{}\r\n\
1206+
Connection: keep-alive\r\n\
1207+
Content-Type: application/json\r\n\
1208+
Content-Length: {}\r\n\
1209+
\r\n\
1210+
{}\r\n\
1211+
", addr.port(), req.as_bytes().len(), req)
1212+
);
1213+
1214+
// then
1215+
assert!(response.headers.contains("connection: close"),
1216+
"Headers missing in {}", response.headers);
1217+
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
1218+
assert_eq!(response.body, world_batch());
1219+
}
1220+
1221+
1222+
11411223
fn invalid_host() -> String {
11421224
"Provided Host header is not whitelisted.\n".into()
11431225
}

http/src/utils.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,22 @@ pub fn cors_allow_headers(
5252
.unwrap_or_else(|_| header::HeaderValue::from_static("unknown"))
5353
)
5454
}
55+
56+
/// Returns an optional value of `Connection` header that should be included in the response.
57+
/// The second parameter defines if server is configured with keep-alive option.
58+
/// Return value of `true` indicates that no `Connection` header should be returned,
59+
/// `false` indicates `Connection: close`.
60+
pub fn keep_alive(
61+
request: &hyper::Request<hyper::Body>,
62+
keep_alive: bool,
63+
) -> bool {
64+
read_header(request, "connection")
65+
.map(|val| match (keep_alive, val) {
66+
// indicate that connection should be closed
67+
(false, _) | (_, "close") => false,
68+
// don't include any headers otherwise
69+
_ => true,
70+
})
71+
// if the client header is not present, close connection if we don't keep_alive
72+
.unwrap_or(keep_alive)
73+
}

0 commit comments

Comments
 (0)