Skip to content

Consider extending the proxy world with an origin #793

Open

Description

@lukewagner

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

  1. lann commented on Sep 8, 2025

    @lann
    Contributor

    I wonder if this could be generalized to allow for multiple upstream "endpoints":

    interface types {
      resource endpoint {
        handle: func(r: request) -> result<response, error-code>;
      }
    }
    
    // Middleware has one abstract "next" endpoint
    world middleware {
      include service;
      import next: func() -> endpoint;
    }
    
    // A router component might look up endpoints by string label (or use WIT templates some day)
    world router {
      include service;
      import get-endpoint: func(name: string) -> result<endpoint>;
    }
    
    // A broadcast interface could implement request racing, traffic mirroring, etc.
    world broadcast {
      include service;
      import endpoints: func() -> list<endpoint>;
    }
    
    // Endpoints could be useful outside of proxy scenarios
    interface untrusted-api-client {
      resource api-client {
        constructor(endpoint: endpont);
        // ...
      }
    }

    Spin achieves something similar to the router example above with "Local Service Chaining", which uses a reserved domain name pattern *.spin.internal to 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 the middleware interface to the proxy world through a reserved domain next.middleware.wasi.alt.

  2. lukewagner commented on Sep 8, 2025

    @lukewagner
    MemberAuthor

    @lann That makes totally sense (assuming you meant middleware et al to be worlds), particularly for the router, broadcast and untrusted-api-client cases 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 N proxy components can be chained together by directly linking one proxy's exported handle function to the other proxy's origin.handle function import (e.g., via wac) without needing to synthesize an (endpoint resource, middleware.next function) pair per proxy. 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 the wasi:http/types.{request,response} resources that would be shared by handle, origin and endpoint. WDYT?

  3. lann commented on Oct 6, 2025

    @lann
    Contributor

    rename proxy to service

    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 future 0.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".

  4. lukewagner commented on Oct 6, 2025

    @lukewagner
    MemberAuthor

    If you or someone else is up for implementing it, that sounds ideal to me.

  5. lann commented on Oct 9, 2025

    @lann
    Contributor

    I'd like to propose something a little different. As we're recognizing this need for two different kinds of handler I 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).

    interface handler { handle: ... }
    interface outgoing-handler { handle: ... }
    world imports {
      import outgoing-handler;
      ...
    }
    world service {
      include imports;
      export handler;
    }
    world middleware {
      include service;
      import handler;
    }

    The "incoming" variant gets the privilege of the bare handler name:

    • (incoming) handler applies to more distinct use cases: "normal" HTTP services, middleware, reverse proxies, etc.
    • outbound-handler represents a sort of "ambient capability" like wasi:filesystem/preopens and I would expect this interface to mostly disappear into ecosystem-standard HTTP client impls.

    proxy is replaced by middleware:

    • I also considered calling this "gateway" (to stick with RFC9110 terms) but ultimately I think "middleware" is the best industry term. Regardless of the new name I think "proxy" is confusing here. A proxy would - I think - just be a service in this proposal, using the outgoing-handler for its origin(s).
    • Since we've renamed the "ambient default client" to outbound-handler, we're free to import handler here, which means middleware composition is just the "standard" wac plug-style name matching.
  6. lann commented on Oct 10, 2025

    @lann
  7. lukewagner commented on Oct 10, 2025

    @lukewagner
    MemberAuthor

    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 service and middleware: it's easy to see how you could have a chain of N middleware components terminating at a service component.

    I struggled for a bit with the fact that middleware is not an RFC 9110 term and in general we try to map everything in wasi-http to RFC 9110. However, service is 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 beside handler which, in the middleware world, is both incoming and outgoing. And, while less common, there will be some situations where I want my component to export outgoing-handler. One alternative idea is a wasi:http/fetcher interface defining a fetch function (with the same function type as handle). This name has some nice properties:

    • for JS guest code running in a component or a wasi-http component running in a browser, fetch would map to fetch
    • if a component exports fetch, 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 case
  8. lann commented on Oct 10, 2025

    @lann
    Contributor

    The one name I'm unsure of is outgoing-handler

    I am certainly not attached to that name. I also considered naming the interface fetch or client. I was trying to keep the handle function 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 forward interface 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

  9. lukewagner commented on Oct 10, 2025

    @lukewagner
    MemberAuthor

    @lann interface client { send: ... } seems pretty good too, imo

  10. tschneidereit commented on Oct 13, 2025

    @tschneidereit
    Member

    I agree: client seems a lot more self-explanatory than the alternatives discussed. I like it!

  11. lann commented on Oct 14, 2025

    @lann
    Contributor

    ☝️ this has been rolled into WebAssembly/wasi-http#199

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P-httpProposal: wasi-http

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions