Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions crates/wasi-http/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,29 @@ impl fmt::Display for Error {
}

impl std::error::Error for Error {}

/// A response that should be returned to the client when a request cannot be
/// turned into a guest request.
#[derive(Debug)]
pub struct ErrorResponse {
status: http::StatusCode,
}

#[expect(missing_docs, reason = "self-explanatory")]
impl ErrorResponse {
pub fn new(status: http::StatusCode) -> Self {
Self { status }
}

pub fn status(&self) -> http::StatusCode {
self.status
}
}

impl fmt::Display for ErrorResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "error response: {}", self.status)
}
}

impl std::error::Error for ErrorResponse {}
21 changes: 19 additions & 2 deletions crates/wasi-http/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,9 @@ where
/// - [`ExpirationError`] if the request expired before it produced a
/// response. See [`WorkerState::on_request_start`] for details.
///
/// - [`ErrorResponse`](crate::ErrorResponse) if the request could not be
/// converted into a guest request.
///
/// - [`TrapOrPanicError`] if the worker responsible for handling the
/// request trapped or panicked before it produced a response. This may be
/// used when a trap occurs but cannot be traced to a specific request,
Expand Down Expand Up @@ -1019,8 +1022,22 @@ impl<'a, T: Send> Prepared<'a, T> {
// producing a response.
let tx = Arc::new(Mutex::new(Some(tx)));

let request =
view(store.data_mut()).new_incoming_request(p2_types::Scheme::Http, request)?;
let request = match view(store.data_mut())
.new_incoming_request(p2_types::Scheme::Http, request)
{
Ok(request) => request,
Err(e) => {
// The request never reached the guest, so there is no
// `response-outparam` to report the failure with, and
// `tx` would otherwise be dropped along with the error.
if let Some(tx) = tx.lock().unwrap().take() {
_ = tx.send(Err(e));
}
wasmtime::bail!(
"request was rejected before it could be turned into a guest request"
);
}
};

let out = view(store.data_mut()).new_response_outparam_from_callback({
let tx = tx.clone();
Expand Down
11 changes: 8 additions & 3 deletions crates/wasi-http/src/p2/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ use crate::p2::{
bindings::http::types::{self, Method, Scheme},
body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody},
};
use crate::{Error, FieldMap, WasiHttpCtxView};
use crate::{Error, ErrorResponse, FieldMap, WasiHttpCtxView};
use bytes::Bytes;
use http_body_util::BodyExt;
use hyper::body::Body;
use wasmtime::Result;
use wasmtime::component::Resource;
use wasmtime::{Result, bail};
use wasmtime_wasi::p2::Pollable;
use wasmtime_wasi::runtime::AbortOnDropJoinHandle;

Expand Down Expand Up @@ -89,7 +89,12 @@ impl WasiHttpCtxView<'_> {
Some(authority) => authority.to_string(),
None => match parts.headers.get(http::header::HOST) {
Some(host) => host.to_str()?.to_string(),
None => bail!("invalid HTTP request missing authority in URI and host header"),
None => {
return Err(wasmtime::Error::msg(
"invalid HTTP request missing authority in URI and host header",
)
.context(ErrorResponse::new(http::StatusCode::BAD_REQUEST)));
}
},
};

Expand Down
18 changes: 12 additions & 6 deletions src/commands/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ use wasmtime::{
use wasmtime_cli_flags::opt::WasmtimeOptionValue;
use wasmtime_wasi::p2::{StreamError, StreamResult};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
use wasmtime_wasi_http::WasiHttpCtx;
use wasmtime_wasi_http::handler::{
HandlerState, Instance, Prepared, Proxy, ProxyHandler, ProxyPre, ShouldAccept,
WorkerExpiration, WorkerState, WorkerStatus,
};
use wasmtime_wasi_http::io::TokioIo;
use wasmtime_wasi_http::{ErrorResponse, WasiHttpCtx};

#[cfg(feature = "debug")]
use crate::commands::run::RunCommand;
Expand Down Expand Up @@ -1273,22 +1273,28 @@ async fn handle_client(
Ok(r) => Ok::<_, Infallible>(r),
Err(e) => {
eprintln!("error: {e:?}");
let error_html = "\
let status = e
.downcast_ref::<ErrorResponse>()
.map(|e| e.status())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let error_html = format!(
"\
<!doctype html>
<html>
<head>
<title>500 Internal Server Error</title>
<title>{status}</title>
</head>
<body>
<center>
<h1>500 Internal Server Error</h1>
<h1>{status}</h1>
<hr>
wasmtime
</center>
</body>
</html>";
</html>"
);
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.status(status)
.header("Content-Type", "text/html; charset=UTF-8")
.body(
Full::new(bytes::Bytes::from(error_html))
Expand Down
43 changes: 43 additions & 0 deletions tests/all/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ mod test_programs {
use std::thread::{self, JoinHandle};
use std::time::Duration;
use test_programs_artifacts::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use wasmtime::{Result, bail, error::Context as _, format_err};

Expand Down Expand Up @@ -2033,6 +2034,48 @@ start a print 1234
Ok(())
}

#[tokio::test]
async fn p2_cli_serve_missing_authority() -> Result<()> {
let server = WasmtimeServe::new(P2_CLI_SERVE_HELLO_WORLD_COMPONENT, |cmd| {
cmd.arg("-Scli");
})?;

// hyper's client always synthesizes a `Host` header for HTTP/1.1
// requests, so write this request out by hand in order to leave both
// the URI authority and the `Host` header off. There is nothing to
// serve such a request with: it is rejected while it is being turned
// into a guest request, before the guest runs at all, so the status
// code comes from the error rather than from a guest response.
let mut stream = TcpStream::connect(server.first_addr()).await?;
stream.write_all(b"GET / HTTP/1.1\r\n\r\n").await?;
// The connection is kept alive, so read until the body has arrived
// rather than until EOF.
let mut response = Vec::new();
let mut buf = [0; 512];
while !response.ends_with(b"</html>") && response.len() < 8192 {
let n = tokio::time::timeout(Duration::from_secs(10), stream.read(&mut buf))
.await
.expect("timed out waiting for a response")?;
if n == 0 {
break;
}
response.extend_from_slice(&buf[..n]);
}
let response = String::from_utf8(response)?;
assert!(
response.starts_with("HTTP/1.1 400"),
"unexpected response: {response:?}",
);
assert!(
response.contains("<h1>400 Bad Request</h1>"),
"unexpected response: {response:?}",
);
drop(stream);

server.finish()?;
Ok(())
}

#[test]
fn p2_cli_argv0() -> Result<()> {
run_wasmtime(&["run", "--argv0=a", P2_CLI_ARGV0, "a"])?;
Expand Down
Loading