Repository navigation
Consider extending the proxy world with an origin #793
Description
A common pattern for HTTP middleware (like express.js or proxy-wasm) is to create a linear chain of handlers that requests flow down (and responses flow back up) where each handler can manipulate (or block) the request/response before passing it on to the next handler in the chain. While processing a request, a handler can make any number of async "side" HTTP requests that don't flow down the linear chain. Thus there are two ways an HTTP request flows out of a handler: (1) to the next handler in the chain and (2) as a side call. E.g., in proxy-wasm, (2) is expressed by proxy_http_call which is quite separate from all the callbacks used for (1).
Ideally, since components and wasi:http are all about composition, we should be able to capture this pattern directly by simply linking components together (e.g, using wac). However, since wasi:http/proxy only has one way to issue outgoing HTTP requests, there's no simple way to distinguish (1) from (2) via linking (something more dynamic/complex is required).
Before the proposed change, one bit of terminology: RFC 9110 defines a "(reverse) proxy" as a kind of "intermediary" that sits between the client and the "origin". Thus, "proxy" implies the existence of a designated "origin".
Based on this, I thought perhaps we should have two worlds in wasi:http:
package wasi:http;
world service {
import imports; // as currently defined
export handler;
}
world proxy {
include service;
import origin;
}
interface origin {
use types.{request, response, error-code};
handle: func(r: request) -> result<response, error-code>;
}or, in summary: rename proxy to service and then define proxy to extend service with an origin (which is structurally the same as handler but with a different interface name). A proxy component thus imports two handle functions and can statically dispatch HTTP requests to either one and forward responses from either one. The distinct handler and origin names allow a WAC user to easily create a middleware chain with side requests.
I'm not especially wed to the name service, so alternative suggestions are welcome, but I kinda like the idea that a component targeting wasi:http/service is a sort of ultra-minimal (serverless) microservice. Moreover, I can see wasi:http/service being included by other bigger service worlds (e.g. wasi:cloud/service).
Thoughts?
Activity
lann commentedon Sep 8, 2025
I wonder if this could be generalized to allow for multiple upstream "endpoints":
Spin achieves something similar to the
routerexample above with "Local Service Chaining", which uses a reserved domain name pattern*.spin.internalto signal to the host that a request should be routed internally to a "sibling" component within a Spin app. A similar approach could be used for compatilibity in some of the scenarios above, by e.g. adapting themiddlewareinterface to theproxyworld through a reserved domainnext.middleware.wasi.alt.lukewagner commentedon Sep 8, 2025
@lann That makes totally sense (assuming you meant
middlewareet al to beworlds), particularly for therouter,broadcastanduntrusted-api-clientcases where there is a dynamic set of endpoints to dispatch to.One nice thing about
wasi:http/proxy(as written in the OP) is that Nproxycomponents can be chained together by directly linking oneproxy's exportedhandlefunction to the otherproxy'sorigin.handlefunction import (e.g., viawac) without needing to synthesize an (endpointresource,middleware.nextfunction) pair perproxy. However, I think we might be in a more-the-merrier situation where we can define all the above, since the meat of the guest and host implementation is associated with thewasi:http/types.{request,response}resources that would be shared byhandle,originandendpoint. WDYT?lann commentedon Oct 6, 2025
Should we consider doing this prior to finalizing
0.3.0? I am broadly in support of this proposal and this would seem to be the only change that couldn't be retrofitted backward-compatibly in a future0.3.x.Even without future additions the name "proxy" has always been a bit confusing to me, I think largely because of this (lack of) distinction between "origin" and "ancillary outbound requests".
lukewagner commentedon Oct 6, 2025
If you or someone else is up for implementing it, that sounds ideal to me.
lann commentedon Oct 9, 2025
I'd like to propose something a little different. As we're recognizing this need for two different kinds of
handlerI think it would be best to (re-)split them into incoming and outgoing variants. While they are abstractly very similar I think in practice there is rarely much benefit to having them be unified (unlike request / response types).The "incoming" variant gets the privilege of the bare
handlername:handlerapplies to more distinct use cases: "normal" HTTP services, middleware, reverse proxies, etc.outbound-handlerrepresents a sort of "ambient capability" likewasi:filesystem/preopensand I would expect this interface to mostly disappear into ecosystem-standard HTTP client impls.proxyis replaced bymiddleware:servicein this proposal, using theoutgoing-handlerfor its origin(s).outbound-handler, we're free to importhandlerhere, which means middleware composition is just the "standard"wac plug-style name matching.lann commentedon Oct 10, 2025
lukewagner commentedon Oct 10, 2025
Ooh, I really like those changes; particularly the part about making the names just match when chaining middleware components. It also nicely syntactically distinguishes the intended roles of
serviceandmiddleware: it's easy to see how you could have a chain of Nmiddlewarecomponents terminating at aservicecomponent.I struggled for a bit with the fact that
middlewareis not an RFC 9110 term and in general we try to map everything in wasi-http to RFC 9110. However,serviceis also not an official RFC 9110 term, and that seemed fine... I think perhaps the principled distinction here is this: because worlds import HTTP and non-HTTP concepts (e.g.,wasi:clocks), we shouldn't expect these worlds to precisely match anything defined in RFC 9110; it's simply out of scope for RFC 9110. Maybe it's even better that they don't attempt to "claim" an RFC 9110 name (like "proxy", "gateway" or "intermediary") since it runs the risk of tying these open-ended RFC 9110 concepts to the particular set of choices we make when defining the world.The one name I'm unsure of is
outgoing-handler; for me it creates some dissonance sitting besidehandlerwhich, in themiddlewareworld, is both incoming and outgoing. And, while less common, there will be some situations where I want my component to exportoutgoing-handler. One alternative idea is awasi:http/fetcherinterface defining afetchfunction (with the same function type ashandle). This name has some nice properties:fetchwould map tofetchfetch, you're not expecting it to implement a middleware function or an HTTP service; it's literally "implementing fetch" which is a more advanced and less common virtualization use caselann commentedon Oct 10, 2025
I am certainly not attached to that name. I also considered naming the interface
fetchorclient. I was trying to keep thehandlefunction name under the possibly-incorrect assumption that it would be easier (with currently tooling) to compose outgoing and incoming handlers if the function name matched; I guess I would otherwise put forwardinterface client { send: ... }as another naming option.Edit: notably, "fetch" does not appear in RFC9110 (apart from "pre-fetched") 🙂
Edit 2: confirmed with Alex that different function names makes it slightly harder but not enough to outweigh other considerations here
lukewagner commentedon Oct 10, 2025
@lann
interface client { send: ... }seems pretty good too, imotschneidereit commentedon Oct 13, 2025
I agree:
clientseems a lot more self-explanatory than the alternatives discussed. I like it!lann commentedon Oct 14, 2025
☝️ this has been rolled into WebAssembly/wasi-http#199