Skip to content

serve: answer 400, not 500, for a request with no authority or Host header - #14432

Merged
pchickey merged 4 commits into
bytecodealliance:mainfrom
xia-chao:serve-host-400
Oct 5, 2026
Merged

pchickey merged 4 commits into
bytecodealliance:mainfrom
xia-chao:serve-host-400

Conversation

@xia-chao

@xia-chao xia-chao commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

wasmtime serve answers 500 when a request has neither a URI authority nor a
Host header. The rejection happens before the guest runs, so the error now
carries the status the client should see, and serve reads it back, defaulting
to 500. That gives the 400 RFC 9112 section 3.2 asks for, and it is not
version-specific: an HTTP/1.0 request without Host gets the same 400.
wasi:http/service is untouched.

…ader

RFC 9112 section 3.2 requires a 400 response to an HTTP/1.1 request that
lacks a Host header field. `wasmtime serve` currently answers 500 for it: the
request is rejected while it is being turned into an incoming request, and
that failure is reported the same way as every other request-handling
failure.

Check for the missing Host in `serve` before dispatching, where the request
is still known to be a malformed client request rather than an internal
error.
@xia-chao
xia-chao requested a review from a team as a code owner September 26, 2026 17:46
@xia-chao
xia-chao requested review from pchickey and removed request for a team September 26, 2026 17:46

@pchickey pchickey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comes down to an architectural decision for whether the guest or host code is responsible for enforcing the RFC invariant. De facto in this implementation, if hyper didn't enforce an RFC invariant, then it would be up to the guest to enforce it. That's not a principled decision, it was just the way things shook out, and other hosts very likely end up handling broken RFC invariants differently than the exact way hyper handles them.

So, we could enforce this particular RFC invariant here, but then the questions are:

  1. why, exactly, do we not enforce it for p3? The comment here doesn't make sense to me, because in p2 the guest is also capable of observing whether authority is none.
  2. what other RFC invariants is our implementation not enforcing in the host? Do we add them all here?
  3. What from this do we need to feed into specification of the wasi-http implementation?

Want to call this out to other maintainers here, cc @lann

@lann

lann commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

If new_incoming_request is going to validate host/authority presence then by spec it should be returning a 400. Pragmatically I'm not sure where that would make a difference.

@xia-chao

xia-chao commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Fair — the comment was wrong. I tested both worlds: the proxy path already fails on a Host-less request, we just turned the 500 into a 400. The service path doesn't fail, it just never gets the host.

@lann

lann commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Could you say more about your motivation for this change?

@xia-chao

Copy link
Copy Markdown
Contributor Author

RFC 9112 §3.2 says an HTTP/1.1 request without Host must get 400, and §3.3 lets the server reject an empty authority, so rejecting is fine — ours just surfaces as a 500 with "worker trapped or panicked", which reads as our bug. Only the status code changes.

@pchickey

Copy link
Copy Markdown
Contributor

@xia-chao Its possible to modify your guest code to give the correct status, correct?

@lann

lann commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

@xia-chao Its possible to modify your guest code to give the correct status, correct?

No, that's the whole problem; wasmtime-wasi-http bails before it gets to the guest:

None => bail!("invalid HTTP request missing authority in URI and host header"),

@xia-chao

Copy link
Copy Markdown
Contributor Author

No — the host rejects it before the guest runs: "invalid HTTP request missing authority in URI and host header". A duplicate or invalid Host does reach the guest.

@xia-chao

Copy link
Copy Markdown
Contributor Author

It's a bug, not a preference.

@pchickey

pchickey commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

OK, I apologize, I missed that, now I understand what both of you are saying.

We don't want to duplicate the logic of detecting this 400 Bad Request. Instead, we should change the place the bad request is detected to also include what status code Wasmtime serve should respond with. So, instead of this patch, the architecture I'd like to see is:

  1. add a pub struct ErrorResponse { status: StatusCode } impl ErrorResponse { pub fn new(status: StatusCode) -> Self {...} pub fn status(&self) -> StatusCode {...} } to wasmtime-wasi-http, with derive Debug, impls Display ("error response: {}"), and impl std::error::Error. This struct has opaque fields so that we can extend it in the future with more, if applicable.
  2. change the bail linked to have, additionally, .context(ErrorResponse::new(StatusCode::BAD_REQUEST))
  3. in the Err case handler in wasmtime-cli's serve.rs, try to downcast the error to an ErrorResponse and get the status, with a default to 500 if the downcast fails. Put that status that into the status code of the response, as well as the html response body. The current hard-coded 500 response body should become a template. Use https://docs.rs/http/latest/http/status/struct.StatusCode.html#method.canonical_reason for the non-numeric description.

@xia-chao
xia-chao requested a review from a team as a code owner September 29, 2026 15:29
@xia-chao
xia-chao requested review from dicej and removed request for a team September 29, 2026 15:29
@xia-chao xia-chao changed the title serve: answer 400, not 500, for an HTTP/1.1 request without a Host header serve: answer 400, not 500, for a request with no authority or Host header Sep 29, 2026
@xia-chao

Copy link
Copy Markdown
Contributor Author

Your three steps are merged, but they don't actually solve the problem: the p2 prepare error gets wrapped into a TrapOrPanicError before serve gets it, so ErrorResponse is only logged and we still return a 500.

To get a 400, we should pass this error back through tx just like instantiation errors do. This touches 6 test cases, but leaves service completely unchanged.

@xia-chao

xia-chao commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Done in fdf05d2. Passing the error back through tx gives us the 400 now;

@pchickey pchickey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks very much for the changes. This round of feedback is mostly that Claude is awfully verbose with comments (you pay by the token, after all) and I think that all of the really obvious ones are best deleted.

Comment thread crates/wasi-http/src/error.rs Outdated
Comment thread crates/wasi-http/src/error.rs Outdated
Comment thread crates/wasi-http/src/error.rs Outdated
Comment thread crates/wasi-http/src/handler.rs Outdated
Comment thread crates/wasi-http/src/handler.rs Outdated
Comment thread crates/wasi-http/src/handler.rs Outdated
Comment thread crates/wasi-http/src/handler.rs Outdated
Comment thread src/commands/serve.rs Outdated
Comment thread src/commands/serve.rs Outdated
Per review: keep the comment that explains why the error is handed back through
tx, and drop the ones that restate the code. ErrorResponse::new and ::status now
carry no doc text, which needs #[expect(missing_docs, reason = "self-explanatory")]
because this crate is #![deny(missing_docs)]. The error is built with
wasmtime::bail!.
@xia-chao

xia-chao commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Done

@pchickey

pchickey commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Thanks!

@pchickey
pchickey added this pull request to the merge queue Oct 5, 2026
Merged via the queue into bytecodealliance:main with commit ffcdcdd Oct 5, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants