Skip to content

Commit 0d07ae2

Browse files
authored
Return 5XX for requests that land in a reused session that crashes (#540)
1 parent f2ea186 commit 0d07ae2

File tree

9 files changed

+98
-74
lines changed

9 files changed

+98
-74
lines changed

cli/tests/integration/reusable_sessions.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,42 @@ viceroy_test!(check_hostcalls_implemented, |is_component| {
2424

2525
Ok(())
2626
});
27+
28+
viceroy_test!(check_crash_causes_5xx, |is_component| {
29+
let test = Test::using_fixture("reusable-sessions.wasm")
30+
.adapt_component(is_component)
31+
.via_hyper();
32+
33+
let reqs = (0..5)
34+
.into_iter()
35+
.map(|n| {
36+
let body = if n == 4 {
37+
"crash!".to_owned()
38+
} else {
39+
n.to_string()
40+
};
41+
Request::post("/").body(body).unwrap()
42+
})
43+
.collect();
44+
45+
let mut resps = test.against_many(reqs).await?;
46+
let errs = resps.split_off(4);
47+
48+
assert_eq!(resps.len(), 4);
49+
assert_eq!(errs.len(), 1);
50+
51+
for (n, resp) in resps.into_iter().enumerate() {
52+
let exp = format!("Response #{}", n + 1);
53+
assert_eq!(resp.status(), StatusCode::OK);
54+
assert_eq!(resp.into_body().read_into_string().await?, exp);
55+
}
56+
57+
for resp in errs.into_iter() {
58+
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
59+
let body = resp.into_body().read_into_string().await?;
60+
let needle = "error while executing at wasm backtrace";
61+
assert!(body.contains(needle), "missing expected string: {body:?}");
62+
}
63+
64+
Ok(())
65+
});

cli/tests/integration/unknown_import_behavior.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ async fn trap_behavior_function_called() -> TestResult {
3333
// key parts that should be relatively stable across invocations and wasmtime
3434
// versions. Fundamentally though, we're still making assertions based on pretty-printed errors,
3535
// so beware of trivial breakages.
36-
assert!(body.contains("error while executing at wasm backtrace"));
37-
assert!(body.contains("unknown_import::main::"));
36+
assert!(
37+
body.contains("error while executing at wasm backtrace"),
38+
"missing expected string: {body:?}"
39+
);
40+
assert!(
41+
body.contains("unknown_import::main::"),
42+
"missing expected string: {body:?}"
43+
);
3844

3945
Ok(())
4046
}

cli/tests/trap-test/src/main.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,8 @@ async fn fatal_error_traps_impl(adapt_core_wasm: bool) -> TestResult {
3737
// Trap error supplied by the Guest.
3838

3939
let body = resp.into_body().read_into_string().await?;
40-
41-
assert_eq!(
42-
body,
43-
"Fatal error: [A fatal error occurred in the test-only implementation of header_values_get]"
44-
);
40+
let needle = "Fatal error: [A fatal error occurred in the test-only implementation of header_values_get]";
41+
assert!(body.contains(needle), "body missing expected string: {body}");
4542

4643
Ok(())
4744
}

src/execute.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -596,12 +596,7 @@ impl ExecuteCtx {
596596
"There was an error handling the request {}",
597597
e.to_string()
598598
);
599-
#[allow(unused_mut)]
600-
let mut response = Response::builder()
601-
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
602-
.body(Body::empty())
603-
.unwrap();
604-
(response, Some(e))
599+
(anyhow_response(&e), Some(e))
605600
}
606601
Err(e) => panic!("failed to run guest: {}", e),
607602
}
@@ -678,7 +673,15 @@ impl ExecuteCtx {
678673

679674
// Ensure the downstream response channel is closed, whether or not a response was
680675
// sent during execution.
681-
store.data_mut().session.close_downstream_response_sender();
676+
let resp = outcome
677+
.as_ref()
678+
.err()
679+
.map(exec_err_to_response)
680+
.unwrap_or_default();
681+
store
682+
.data_mut()
683+
.session
684+
.close_downstream_response_sender(resp);
682685

683686
let request_duration = Instant::now().duration_since(start_timestamp);
684687

@@ -745,7 +748,12 @@ impl ExecuteCtx {
745748

746749
// Ensure the downstream response channel is closed, whether or not a response was
747750
// sent during execution.
748-
store.data_mut().close_downstream_response_sender();
751+
let resp = outcome
752+
.as_ref()
753+
.err()
754+
.map(exec_err_to_response)
755+
.unwrap_or_default();
756+
store.data_mut().close_downstream_response_sender(resp);
749757

750758
let request_duration = Instant::now().duration_since(start_timestamp);
751759

@@ -830,7 +838,9 @@ impl ExecuteCtx {
830838

831839
// Ensure the downstream response channel is closed, whether or not a response was
832840
// sent during execution.
833-
store.data_mut().close_downstream_response_sender();
841+
store
842+
.data_mut()
843+
.close_downstream_response_sender(Response::default());
834844

835845
// We don't do anything with any response on the receiver, but
836846
// it's important to keep it alive until after the program has
@@ -986,17 +996,24 @@ fn write_profile(store: &mut wasmtime::Store<WasmCtx>, guest_profile_path: Optio
986996
}
987997

988998
fn guest_result_to_response(resp: Response<Body>, err: Option<anyhow::Error>) -> Response<Body> {
989-
if let Some(err) = err {
990-
let body = err.root_cause().to_string();
991-
Response::builder()
992-
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
993-
.body(Body::from(body.as_bytes()))
994-
.unwrap()
999+
err.as_ref().map(anyhow_response).unwrap_or(resp)
1000+
}
1001+
1002+
fn exec_err_to_response(err: &ExecutionError) -> Response<Body> {
1003+
if let ExecutionError::WasmTrap(e) = err {
1004+
anyhow_response(e)
9951005
} else {
996-
resp
1006+
panic!("failed to run guest: {err}")
9971007
}
9981008
}
9991009

1010+
fn anyhow_response(err: &anyhow::Error) -> Response<Body> {
1011+
Response::builder()
1012+
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
1013+
.body(Body::from(format!("{err:?}").into_bytes()))
1014+
.unwrap()
1015+
}
1016+
10001017
impl Drop for ExecuteCtx {
10011018
fn drop(&mut self) {
10021019
if let Some(join_handle) = self.epoch_increment_thread.take() {

src/linking.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
33
use {
44
crate::{
5-
config::ExperimentalModule, execute::ExecuteCtx, logging::LogEndpoint, session::Session,
6-
wiggle_abi, Error,
5+
body::Body, config::ExperimentalModule, execute::ExecuteCtx, logging::LogEndpoint,
6+
session::Session, wiggle_abi, Error,
77
},
8+
hyper::Response,
89
std::collections::HashSet,
910
wasmtime::{GuestProfiler, Linker, Store, StoreLimits, StoreLimitsBuilder, UpdateDeadline},
1011
wasmtime_wasi::preview1::WasiP1Ctx,
@@ -134,8 +135,8 @@ impl ComponentCtx {
134135
&self.limiter
135136
}
136137

137-
pub fn close_downstream_response_sender(&mut self) {
138-
self.session.close_downstream_response_sender()
138+
pub fn close_downstream_response_sender(&mut self, resp: Response<Body>) {
139+
self.session.close_downstream_response_sender(resp)
139140
}
140141

141142
/// Initialize a new [`Store`][store], given an [`ExecuteCtx`][ctx].
@@ -232,8 +233,8 @@ impl WasmCtx {
232233
}
233234

234235
impl WasmCtx {
235-
pub fn close_downstream_response_sender(&mut self) {
236-
self.session.close_downstream_response_sender()
236+
pub fn close_downstream_response_sender(&mut self, resp: Response<Body>) {
237+
self.session.close_downstream_response_sender(resp)
237238
}
238239
}
239240

src/session.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,10 @@ impl Session {
267267
self.downstream_resp.redirect_to_pushpin(redirect_info)
268268
}
269269

270-
/// Close the downstream response sender, potentially without sending any response.
271-
pub fn close_downstream_response_sender(&mut self) {
272-
self.downstream_resp.close()
270+
/// Ensure the downstream response sender is closed, and send the provided response if it
271+
/// isn't.
272+
pub fn close_downstream_response_sender(&mut self, resp: Response<Body>) {
273+
let _ = self.downstream_resp.send(resp);
273274
}
274275

275276
// ----- Bodies API -----

src/session/downstream.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ impl DownstreamResponseState {
8585
}
8686

8787
/// Close the `DownstreamResponse`, potentially without sending any response.
88+
#[allow(unused)]
8889
pub fn close(&mut self) {
8990
*self = DownstreamResponseState::Closed;
9091
}

test-fixtures/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ publish = false
1111
anyhow = "1.0.86"
1212
base64 = "0.21.2"
1313
bitflags = "1.3.2"
14-
fastly = "0.11.5"
15-
fastly-shared = "0.11.5"
16-
fastly-sys = "0.11.5"
14+
fastly = "0.11.7"
15+
fastly-shared = "0.11.7"
16+
fastly-sys = "0.11.7"
1717
hex-literal = "0.4.1"
1818
bytes = "1.0.0"
1919
http = "1.1.0"

test-fixtures/src/bin/reusable-sessions.rs

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,7 @@
33
use fastly::{Request, Response};
44
use fastly::handle::{BodyHandle, RequestHandle};
55
use fastly_shared::{FastlyStatus, INVALID_REQUEST_HANDLE};
6-
7-
pub type RequestPromiseHandle = u32;
8-
9-
#[derive(Default)]
10-
#[repr(C)]
11-
pub struct NextRequestOptions {
12-
pub reserved: u64,
13-
}
14-
15-
bitflags::bitflags! {
16-
/// Request options.
17-
#[derive(Default)]
18-
#[repr(transparent)]
19-
pub struct NextRequestOptionsMask: u32 {
20-
const RESERVED = 1 << 0;
21-
}
22-
}
23-
24-
#[link(wasm_import_module = "fastly_http_downstream")]
25-
extern "C" {
26-
#[link_name = "next_request"]
27-
pub fn next_request(
28-
options_mask: NextRequestOptionsMask,
29-
options: *const NextRequestOptions,
30-
handle_out: *mut RequestPromiseHandle,
31-
) -> FastlyStatus;
32-
33-
#[link_name = "next_request_wait"]
34-
pub fn next_request_wait(
35-
handle: RequestPromiseHandle,
36-
req_handle_out: *mut fastly_sys::RequestHandle,
37-
body_handle_out: *mut fastly_sys::BodyHandle,
38-
) -> FastlyStatus;
39-
40-
#[link_name = "next_request_abandon"]
41-
pub fn next_request_abandon(
42-
handle: RequestPromiseHandle,
43-
) -> FastlyStatus;
44-
}
6+
use fastly_sys::fastly_http_downstream::*;
457

468
fn is_ready(handle: u32) -> bool {
479
let mut ready_out: u32 = 0;

0 commit comments

Comments
 (0)