How to Fix: wasmtime-wasi-http includes absolute URI in HTTP requests
wasmtime-wasi-http includes absolute URI in HTTP requests: root cause and fix
If your server suddenly rejects requests from wasmtime-wasi-http because the request line contains a full URL like GET http://example.com/path HTTP/1.1 instead of the expected origin-form GET /path HTTP/1.1, the bug is usually a mismatch between proxy-style absolute-form requests and what an origin server expects.
In practice, this issue shows up when a component using WASI HTTP sends an outbound request and the lower HTTP layer serializes the target as an absolute URI. That format is valid in some contexts, especially when talking to an HTTP proxy, but many application servers, frameworks, and test harnesses expect only the path and query in the request target.
Understanding the Root Cause
HTTP request targets can be encoded in multiple forms. The most relevant two here are:
- origin-form:
GET /users?id=1 HTTP/1.1 - absolute-form:
GET http://example.com/users?id=1 HTTP/1.1
For direct requests to a normal HTTP origin server, clients should generally send origin-form. absolute-form is primarily intended for requests sent to an HTTP proxy. When wasmtime-wasi-http includes the full URI in a direct request, some servers interpret that as malformed, route incorrectly, or fail strict validation.
Technically, the bug usually appears in one of these places:
- The request object stores a fully qualified URL and the transport layer serializes it without stripping scheme and authority.
- The runtime assumes proxy semantics even when making a direct connection.
- An adapter between WASI HTTP types and the underlying HTTP client maps the outgoing target incorrectly.
- Tests pass against permissive servers but fail against stricter implementations such as reverse proxies, custom HTTP parsers, or framework test servers.
This matters because downstream software often uses the request target for routing, signature verification, host handling, or security policy enforcement. An absolute URI in the wrong place can break all of those.
Step-by-Step Solution
The fix is to ensure the outbound HTTP request line uses origin-form unless the request is explicitly going through a proxy. In other words, preserve the Host header or authority separately, but serialize only path + query as the request target.
1. Confirm the faulty request format
First, capture the raw outbound request with a local test server, proxy, or HTTP trace tool. You are looking for this kind of incorrect request line:
GET http://localhost:3000/api/data?x=1 HTTP/1.1
Host: localhost:3000
The correct direct request should look like this:
GET /api/data?x=1 HTTP/1.1
Host: localhost:3000
2. Locate where the request target is serialized
In a typical Rust-based stack around Wasmtime, inspect the code path that converts a WASI HTTP outgoing request into a lower-level HTTP client request. If the code passes the full URL string as the URI to a serializer that emits request lines directly, that is the bug source.
The logic should conceptually do this:
let scheme = request.scheme(); // http or https
let authority = request.authority(); // example.com:80
let path_with_query = request.path_with_query().unwrap_or("/");
// For direct origin requests, send only path + query as request target.
let request_target = path_with_query;
// Preserve authority in Host header or equivalent URI authority field.
set_host_header(authority);
send_request_line(method, request_target);
3. Only use absolute-form for real proxy requests
If your runtime supports explicit HTTP proxying, gate the behavior behind a proxy check:
let request_target = if using_http_proxy {
full_absolute_uri(&scheme, &authority, &path_with_query)
} else {
path_with_query.to_string()
};
This keeps standards-compliant behavior for proxies while fixing direct origin requests.
4. Patch the request-building layer
If you are maintaining the affected code, the practical patch usually looks like this at a high level:
fn build_outbound_request(req: OutgoingRequest, using_http_proxy: bool) -> SerializedRequest {
let method = req.method();
let scheme = req.scheme().unwrap_or("http");
let authority = req.authority().expect("authority required");
let path = req.path_with_query().unwrap_or("/");
let target = if using_http_proxy {
format!("{}://{}{}", scheme, authority, path)
} else {
path.to_string()
};
let mut headers = req.headers().clone();
if !headers.contains("host") {
headers.insert("host", authority.to_string());
}
SerializedRequest {
method,
target,
headers,
body: req.into_body(),
}
}
5. Add regression tests
This bug should always be covered by tests because it can quietly reappear during refactors.
#[test]
fn direct_http_request_uses_origin_form() {
let req = make_request(
"GET",
"http",
"example.com:80",
"/health?full=1"
);
let serialized = build_outbound_request(req, false);
assert_eq!(serialized.target, "/health?full=1");
assert_eq!(serialized.headers.get("host"), Some(&"example.com:80".to_string()));
}
#[test]
fn proxied_http_request_uses_absolute_form() {
let req = make_request(
"GET",
"http",
"example.com:80",
"/health?full=1"
);
let serialized = build_outbound_request(req, true);
assert_eq!(serialized.target, "http://example.com:80/health?full=1");
}
6. Verify against a strict server
After patching, test against a server that does not tolerate absolute-form on direct connections. This is important because permissive servers may hide the problem. If you are using a framework test server, assert both the received path and host separately.
assert_eq!(received.path(), "/health");
assert_eq!(received.query(), Some("full=1"));
assert_eq!(received.host(), Some("example.com:80"));
7. Update documentation for runtime behavior
If this bug affected your runtime integration, document the rule clearly: direct requests use origin-form; proxied requests use absolute-form. That prevents future confusion for contributors working at the WASI, HTTP client, or transport boundaries.
Common Edge Cases
- Reverse proxies in front of the app: Some reverse proxies normalize request targets, while others reject unexpected absolute-form requests. Always test both direct and proxied topologies.
- HTTPS requests: Even though TLS is involved, the serialized HTTP request line for a direct request should still usually use origin-form after the connection is established.
- Missing path: If the URL path is empty, send
/, not an empty string. - Query string handling: Preserve
?key=valueexactly. A fix that strips scheme and authority must not accidentally drop the query. - Host header mismatches: Do not derive routing from the absolute URI and then forget to set the Host header correctly.
- IPv6 authorities: Bracketed hosts like
[::1]:8080must remain valid in the authority and Host header while the target remains just the path. - CONNECT or special methods: Some methods use different request-target forms under the HTTP spec. Do not blindly apply origin-form logic to methods with special semantics.
- Signed requests: If your app signs requests based on the request-target, changing absolute-form to origin-form may require updating signature generation and verification logic.
FAQ
Why is an absolute URI sometimes valid in HTTP requests?
It is valid mainly for proxy requests. A proxy needs the full destination URI, so clients can send absolute-form. For direct requests to an origin server, the usual form is only the path and query.
Why do some servers accept the buggy request while others fail?
Some servers and frameworks are permissive and normalize the target internally. Others are strict, especially security-focused parsers, reverse proxies, and low-level HTTP servers. That is why this issue can appear environment-specific.
Can I fix this by rewriting the URL at the application layer?
Usually no, not reliably. The correct fix belongs in the request serialization layer where the request line is constructed. Application code should provide scheme, authority, and path components, but the transport should choose the correct request-target form.
For reference while reviewing behavior, compare your implementation against the HTTP semantics in the relevant RFC and inspect the current Wasmtime and WASI HTTP integration code in the project repository on GitHub. If you are preparing a patch for the reported issue, keep the change minimal: preserve existing proxy behavior, switch direct requests to origin-form, and add regression tests that assert the exact serialized request target.