Offchain worker: Enable http2 and improve logging (#10305)

* Offchain worker: Enable http2 and improve logging

Apparently some webpages now return http2 by default and that silently breaks the offchain http
extension. The solution to this is to enable the `http2` feature of hyper. Besides that, this pr
improves the logging to make it easier to debug such errors.

* FMT

* Adds http2 test
This commit is contained in:
Bastian Köcher
2021-11-19 12:33:28 +01:00
committed by GitHub
parent 34bc24624a
commit 090b55b791
5 changed files with 172 additions and 55 deletions
+97 -22
View File
@@ -33,7 +33,6 @@ use fnv::FnvHashMap;
use futures::{channel::mpsc, future, prelude::*};
use hyper::{client, Body, Client as HyperClient};
use hyper_rustls::HttpsConnector;
use log::error;
use once_cell::sync::Lazy;
use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use sp_core::offchain::{HttpError, HttpRequestId, HttpRequestStatus, Timestamp};
@@ -46,6 +45,8 @@ use std::{
task::{Context, Poll},
};
const LOG_TARGET: &str = "offchain-worker::http";
/// Wrapper struct used for keeping the hyper_rustls client running.
#[derive(Clone)]
pub struct SharedClient(Arc<Lazy<HyperClient<HttpsConnector<client::HttpConnector>, Body>>>);
@@ -146,13 +147,24 @@ impl HttpApi {
match self.next_id.0.checked_add(1) {
Some(new_id) => self.next_id.0 = new_id,
None => {
error!("Overflow in offchain worker HTTP request ID assignment");
tracing::error!(
target: LOG_TARGET,
"Overflow in offchain worker HTTP request ID assignment"
);
return Err(())
},
};
self.requests
.insert(new_id, HttpApiRequest::NotDispatched(request, body_sender));
tracing::error!(
target: LOG_TARGET,
id = %new_id.0,
%method,
%uri,
"Requested started",
);
Ok(new_id)
}
@@ -168,11 +180,14 @@ impl HttpApi {
_ => return Err(()),
};
let name = hyper::header::HeaderName::try_from(name).map_err(drop)?;
let value = hyper::header::HeaderValue::try_from(value).map_err(drop)?;
let header_name = hyper::header::HeaderName::try_from(name).map_err(drop)?;
let header_value = hyper::header::HeaderValue::try_from(value).map_err(drop)?;
// Note that we're always appending headers and never replacing old values.
// We assume here that the user knows what they're doing.
request.headers_mut().append(name, value);
request.headers_mut().append(header_name, header_value);
tracing::debug!(target: LOG_TARGET, id = %request_id.0, %name, %value, "Added header to request");
Ok(())
}
@@ -207,7 +222,7 @@ impl HttpApi {
sender.send_data(hyper::body::Bytes::from(chunk.to_owned())),
)
.map_err(|_| {
error!("HTTP sender refused data despite being ready");
tracing::error!(target: "offchain-worker::http", "HTTP sender refused data despite being ready");
HttpError::IoError
})
};
@@ -215,6 +230,7 @@ impl HttpApi {
loop {
request = match request {
HttpApiRequest::NotDispatched(request, sender) => {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, "Added new body chunk");
// If the request is not dispatched yet, dispatch it and loop again.
let _ = self
.to_worker
@@ -225,14 +241,20 @@ impl HttpApi {
HttpApiRequest::Dispatched(Some(mut sender)) => {
if !chunk.is_empty() {
match poll_sender(&mut sender) {
Err(HttpError::IoError) => return Err(HttpError::IoError),
Err(HttpError::IoError) => {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, "Encountered io error while trying to add new chunk to body");
return Err(HttpError::IoError)
},
other => {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, res = ?other, "Added chunk to body");
self.requests
.insert(request_id, HttpApiRequest::Dispatched(Some(sender)));
return other
},
}
} else {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, "Finished writing body");
// Writing an empty body is a hint that we should stop writing. Dropping
// the sender.
self.requests.insert(request_id, HttpApiRequest::Dispatched(None));
@@ -250,14 +272,20 @@ impl HttpApi {
.as_mut()
.expect("Can only enter this match branch if Some; qed"),
) {
Err(HttpError::IoError) => return Err(HttpError::IoError),
Err(HttpError::IoError) => {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, "Encountered io error while trying to add new chunk to body");
return Err(HttpError::IoError)
},
other => {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, res = ?other, "Added chunk to body");
self.requests
.insert(request_id, HttpApiRequest::Response(response));
return other
},
}
} else {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, "Finished writing body");
// Writing an empty body is a hint that we should stop writing. Dropping
// the sender.
self.requests.insert(
@@ -271,13 +299,18 @@ impl HttpApi {
}
},
HttpApiRequest::Fail(_) =>
// If the request has already failed, return without putting back the request
// in the list.
return Err(HttpError::IoError),
HttpApiRequest::Fail(error) => {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, ?error, "Request failed");
// If the request has already failed, return without putting back the request
// in the list.
return Err(HttpError::IoError)
},
v @ HttpApiRequest::Dispatched(None) |
v @ HttpApiRequest::Response(HttpApiRequestRp { sending_body: None, .. }) => {
tracing::debug!(target: LOG_TARGET, id = %request_id.0, "Body sending already finished");
// We have already finished sending this body.
self.requests.insert(request_id, v);
return Err(HttpError::Invalid)
@@ -350,8 +383,19 @@ impl HttpApi {
// Requests in "fail" mode are purged before returning.
debug_assert_eq!(output.len(), ids.len());
for n in (0..ids.len()).rev() {
if let HttpRequestStatus::IoError = output[n] {
self.requests.remove(&ids[n]);
match output[n] {
HttpRequestStatus::IoError => {
self.requests.remove(&ids[n]);
},
HttpRequestStatus::Invalid => {
tracing::debug!(target: LOG_TARGET, id = %ids[n].0, "Unknown request");
},
HttpRequestStatus::DeadlineReached => {
tracing::debug!(target: LOG_TARGET, id = %ids[n].0, "Deadline reached");
},
HttpRequestStatus::Finished(_) => {
tracing::debug!(target: LOG_TARGET, id = %ids[n].0, "Request finished");
},
}
}
return output
@@ -388,20 +432,23 @@ impl HttpApi {
);
},
None => {}, // can happen if we detected an IO error when sending the body
_ => error!("State mismatch between the API and worker"),
_ =>
tracing::error!(target: "offchain-worker::http", "State mismatch between the API and worker"),
}
},
Some(WorkerToApi::Fail { id, error }) => match self.requests.remove(&id) {
Some(HttpApiRequest::Dispatched(_)) => {
tracing::debug!(target: LOG_TARGET, id = %id.0, ?error, "Request failed");
self.requests.insert(id, HttpApiRequest::Fail(error));
},
None => {}, // can happen if we detected an IO error when sending the body
_ => error!("State mismatch between the API and worker"),
_ =>
tracing::error!(target: "offchain-worker::http", "State mismatch between the API and worker"),
},
None => {
error!("Worker has crashed");
tracing::error!(target: "offchain-worker::http", "Worker has crashed");
return ids.iter().map(|_| HttpRequestStatus::IoError).collect()
},
}
@@ -474,7 +521,7 @@ impl HttpApi {
},
Err(err) => {
// This code should never be reached unless there's a logic error somewhere.
error!("Failed to read from current read chunk: {:?}", err);
tracing::error!(target: "offchain-worker::http", "Failed to read from current read chunk: {:?}", err);
return Err(HttpError::IoError)
},
}
@@ -719,7 +766,10 @@ mod tests {
// Returns an `HttpApi` whose worker is ran in the background, and a `SocketAddr` to an HTTP
// server that runs in the background as well.
macro_rules! build_api_server {
() => {{
() => {
build_api_server!(hyper::Response::new(hyper::Body::from("Hello World!")))
};
( $response:expr ) => {{
let hyper_client = SHARED_CLIENT.clone();
let (api, worker) = http(hyper_client.clone());
@@ -736,9 +786,7 @@ mod tests {
// otherwise the tests are flaky.
let _ = req.into_body().collect::<Vec<_>>().await;
Ok::<_, Infallible>(hyper::Response::new(hyper::Body::from(
"Hello World!",
)))
Ok::<_, Infallible>($response)
},
))
}),
@@ -776,6 +824,33 @@ mod tests {
assert_eq!(&buf[..n], b"Hello World!");
}
#[test]
fn basic_http2_localhost() {
let deadline = timestamp::now().add(Duration::from_millis(10_000));
// Performs an HTTP query to a background HTTP server.
let (mut api, addr) = build_api_server!(hyper::Response::builder()
.version(hyper::Version::HTTP_2)
.body(hyper::Body::from("Hello World!"))
.unwrap());
let id = api.request_start("POST", &format!("http://{}", addr)).unwrap();
api.request_write_body(id, &[], Some(deadline)).unwrap();
match api.response_wait(&[id], Some(deadline))[0] {
HttpRequestStatus::Finished(200) => {},
v => panic!("Connecting to localhost failed: {:?}", v),
}
let headers = api.response_headers(id);
assert!(headers.iter().any(|(h, _)| h.eq_ignore_ascii_case(b"Date")));
let mut buf = vec![0; 2048];
let n = api.response_read_body(id, &mut buf, Some(deadline)).unwrap();
assert_eq!(&buf[..n], b"Hello World!");
}
#[test]
fn request_start_invalid_call() {
let (mut api, addr) = build_api_server!();