Skip to content

add Wit interfaces #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 4, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
# WASI HTTP

(This is a placeholder so there can be a PR to fill in the contents.)
A proposed [WebAssembly System Interface](https://github.com/WebAssembly/WASI) API.

This proposal currently only contains the proposed Wit interfaces with light
explanation in comments; more work is necessary to fully document the proposal.
The Wit comments annotate where the proposed interface is expected to change in
the short term (for Preview2) once resources and handles are re-added to Wit,
and then after that (for Preview2) once native stream support is added to the
Component Model and Wit.

The `wit` directory currently validates and can generate bindings with:
```
wit-bindgen c wit/ --world proxy
```
or can be manipulated in other ways with:
```
wasm-tools component wit wit/ ...
```

The HTTP proposal depends on the WASI IO and Logging proposals. For simplicity,
the Wit files for these proposals are currently copied into the `wit/deps`
directory and will be updated periodically to match their respective proposals.
As the Wit tooling develops, we should be able to avoid this form of manual
vendoring.

### Current Phase

wasi-http is currently in [Phase 1](https://github.com/WebAssembly/WASI/blob/main/Proposals.md).

### Champions

Piotr Sikora, Jiaxiao Zhou, Dan Chiarlone, David Justice

### TODO

This readme needs to be expanded to cover a number of additional fields suggested in the
[WASI Proposal template](https://github.com/WebAssembly/wasi-proposal-template).
128 changes: 128 additions & 0 deletions wit/deps/io/streams.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
default interface streams {
/// An error type returned from a stream operation. Currently this
/// doesn't provide any additional information.
record stream-error {}

/// An input bytestream. In the future, this will be replaced by handle
/// types.
///
/// This conceptually represents a `stream<u8, _>`. It's temporary
/// scaffolding until component-model's async features are ready.
///
/// And at present, it is a `u32` instead of being an actual handle, until
/// the wit-bindgen implementation of handles and resources is ready.
type input-stream = u32

/// Read bytes from a stream.
///
/// This function returns a list of bytes containing the data that was
/// read, along with a bool indicating whether the end of the stream
/// was reached. The returned list will contain up to `len` bytes; it
/// may return fewer than requested, but not more.
///
/// Once a stream has reached the end, subsequent calls to read or
/// `skip` will always report end-of-stream rather than producing more
/// data.
///
/// If `len` is 0, it represents a request to read 0 bytes, which should
/// always succeed, assuming the stream hasn't reached its end yet, and
/// return an empty list.
///
/// The len here is a `u64`, but some callees may not be able to allocate
/// a buffer as large as that would imply.
/// FIXME: describe what happens if allocation fails.
read: func(
/// The stream to read from
src: input-stream,
/// The maximum number of bytes to read
len: u64
) -> result<tuple<list<u8>, bool>, stream-error>

/// Skip bytes from a stream.
///
/// This is similar to the `read` function, but avoids copying the
/// bytes into the instance.
///
/// Once a stream has reached the end, subsequent calls to read or
/// `skip` will always report end-of-stream rather than producing more
/// data.
///
/// This function returns the number of bytes skipped, along with a bool
/// indicating whether the end of the stream was reached. The returned
/// value will be at most `len`; it may be less.
skip: func(
/// The stream to skip in
src: input-stream,
/// The maximum number of bytes to skip.
len: u64,
) -> result<tuple<u64, bool>, stream-error>

/// An output bytestream. In the future, this will be replaced by handle
/// types.
///
/// This conceptually represents a `stream<u8, _>`. It's temporary
/// scaffolding until component-model's async features are ready.
///
/// And at present, it is a `u32` instead of being an actual handle, until
/// the wit-bindgen implementation of handles and resources is ready.
type output-stream = u32

/// Write bytes to a stream.
///
/// This function returns a `u64` indicating the number of bytes from
/// `buf` that were written; it may be less than the full list.
write: func(
/// The stream to write to
dst: output-stream,
/// Data to write
buf: list<u8>
) -> result<u64, stream-error>

/// Write a single byte multiple times to a stream.
///
/// This function returns a `u64` indicating the number of copies of
/// `byte` that were written; it may be less than `len`.
write-repeated: func(
/// The stream to write to
dst: output-stream,
/// The byte to write
byte: u8,
/// The number of times to write it
len: u64
) -> result<u64, stream-error>

/// Read from one stream and write to another.
///
/// This function returns the number of bytes transferred; it may be less
/// than `len`.
splice: func(
/// The stream to write to
dst: output-stream,
/// The stream to read from
src: input-stream,
/// The number of bytes to splice
len: u64,
) -> result<tuple<u64, bool>, stream-error>

/// Forward the entire contents of an input stream to an output stream.
///
/// This function repeatedly reads from the input stream and writes
/// the data to the output stream, until the end of the input stream
/// is reached, or an error is encountered.
///
/// This function returns the number of bytes transferred.
forward: func(
/// The stream to write to
dst: output-stream,
/// The stream to read from
src: input-stream
) -> result<u64, stream-error>

/// Dispose of the specified input-stream, after which it may no longer
/// be used.
drop-input-stream: func(f: input-stream)

/// Dispose of the specified output-stream, after which it may no longer
/// be used.
drop-output-stream: func(f: output-stream)
}
31 changes: 31 additions & 0 deletions wit/deps/logging/handler.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// # WASI Logging API
///
/// WASI Logging is a logging API intended to let users emit log messages with
/// simple priority levels and context values.
default interface handler {
/// A log level, describing a kind of message.
enum level {
/// Describes messages about the values of variables and the flow of control
/// within a program.
trace,

/// Describes messages likely to be of interest to someone debugging a program.
debug,

/// Describes messages likely to be of interest to someone monitoring a program.
info,

/// Describes messages indicating hazardous situations.
warn,

/// Describes messages indicating serious errors.
error,
}

/// Emit a log message.
///
/// A log message has a `level` describing what kind of message is being sent,
/// a context, which is an uninterpreted string meant to help consumers group
/// similar messages, and a string containing the message text.
log: func(level: level, context: string, message: string)
}
21 changes: 21 additions & 0 deletions wit/incoming-handler.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// The `wasi:http/incoming-handler` interface is meant to be exported by
// components and called by the host in response to a new incoming HTTP
// response.
//
// NOTE: in Preview3, this interface will be merged with
// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface
// that takes a `request` parameter and returns a `response` result.
//
default interface incoming-handler {
Copy link
Contributor

@brendandburns brendandburns Feb 17, 2023

Choose a reason for hiding this comment

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

Do we want to split this out into separate PRs? I feel like it's going to be a heavy lift to do both client and server implementations in a single PR.

Of course, if it's ok to only have a partial implementation, that works too.

use pkg.types.{incoming-request, response-outparam}

// The `handle` function takes an outparam instead of returning its response
// so that the component may stream its response while streaming any other
// request or response bodies. The callee MUST write a response to the
// `response-out` and then finish the response before returning. The `handle`
// function is allowed to continue execution after finishing the response's
// output stream. While this post-response execution is taken off the
// critical path, since there is no return value, there is no way to report
// its success or failure.
handle: func(request: incoming-request, response-out: response-outparam)
}
15 changes: 15 additions & 0 deletions wit/outgoing-handler.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// The `wasi:http/outgoing-handler` interface is meant to be imported by
// components and implemented by the host.
//
// NOTE: in Preview3, this interface will be merged with
// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface
// that takes a `request` parameter and returns a `response` result.
//
default interface outgoing-handler {
use pkg.types.{outgoing-request, incoming-response, error}

// The parameter and result types of the `handle` function allow the caller
// to concurrently stream the bodies of the outgoing request and the incoming
// response.
handle: func(request: outgoing-request) -> result<incoming-response, error>
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I find those pairings (incoming-request and outgoing-response, and outgoing-request and incoming-response) very non-intuitive and a bit redundant.

Could we consider the same prefix on the same connection, e.g. downstream-request and downstream-response, and upstream-request and upstream-response?

Copy link
Contributor

Choose a reason for hiding this comment

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

RFC 9110 suggests inbound and outbound as the preferred terms here, and defines upstream and downstream differently than you are using them: https://www.rfc-editor.org/rfc/rfc9110#name-intermediaries

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, but the popular implementations (e.g. NGINX and Envoy) disagree with the RFC about upstream and downstream, and I'm not aware of anything that actually uses the RFC terms.

I considered suggesting inbound and outbound, but it's yet another term, and it's actually used in service mesh environment to describe traffic direction in a global sense, and not from the perspective of a local endpoint.

Copy link
Contributor

Choose a reason for hiding this comment

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

Our experience at fastly was that upstream and downstream were so confusing internally, and to our customers, that we stopped using those terms throughout our api and docs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What did you end up replacing it with? I'm not a fan of downstream / upstream myself (but mostly because the RFCs disagree with implementations), so I'm fine with other terms.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed that upstream/downstream is extremely confusing in practice (as evidenced by my own misuse of it below). I saw incoming/outgoing in one of the other proposals and it seemed more clear when you think of it from the component author's perspective. That being said, note that all incoming-/outgoing- prefixes would go away in Preview3 when we get to merge all these resources/interfaces.

Copy link
Contributor

Choose a reason for hiding this comment

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

Most programming languages have standardized on request/response without incoming/outgoing b/c the context is pretty obvious from whether you are implementing a server or making a client call and programmers don't really get confused about that.

I think we should delete incoming/outgoing or downstream/upstream and just stick to request/response with the context providing the direction.

Copy link
Member Author

Choose a reason for hiding this comment

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

That is definitely the goal with Preview 3. However, in a Preview 2 timeframe, where we want to do concurrent streaming of requests/responses and have blocking poll_oneoff, the concrete signatures of the request/response resources have to be different depending on whether you are reading from them or writing to them, so we unfortunately can't wait and infer when the request or response hits a call boundary. I do believe it's possible to implement source-language libraries which are direction-agnostic (e.g., JS Response) in terms of these these directional resource types, though, so that this is abstracted from the developer (e.g., when constructing a Response, the impl would internally call new-outgoing-response...). Avoiding this extra work and complexity is one of the main motivations for adding future/stream (and the stack-switching required to implement these in the underlying implementation).

32 changes: 32 additions & 0 deletions wit/proxy.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// The `wasi:http/proxy` world captures a widely-implementable intersection of
// hosts that includes HTTP forward and reverse proxies. Components targeting
// this world may concurrently stream in and out any number of incoming and
// outgoing HTTP requests.
default world proxy {

// This is the default logging handler to use when user code simply wants to
// log to a developer-facing console (e.g., via `console.log()`).
import console: logging.handler

// TODO: add `import metrics: metrics.counters`

// This is the default handler to use when user code simply wants to make an
// HTTP request (e.g., via `fetch()`) but doesn't otherwise specify a
// particular handler.
import default-upstream-HTTP: pkg.outgoing-handler
Copy link
Collaborator

Choose a reason for hiding this comment

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

HTTP world is already overloaded with terms regarding direction, with upstream/downstream often meaning opposite things in different documents and software. Could we avoid using multiple terms describing the same thing in this spec? Maybe let's stick to upstream/downstream, so this should be changed to:
import default-upstream-HTTP: pkg.upstream-handler

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, my mistake mixing and then inverting terminologies. Fixed


// TODO: Once the underlying Wit template machinery is implemented, add:
//
// import upstreams: interface {
// *: pkg.outgoing-handler
// }
//
// which will allow a component to import any number of non-default backends
// that HTTP requests can be dispatched to.

// The host delivers incoming HTTP requests to a component by calling the
// `handle` function of this exported interface. A host may arbitrarily reuse
// or not reuse component instance when delivering incoming HTTP requests and
// thus a component must be able to handle 0..N calls to `handle`.
export HTTP: pkg.incoming-handler
}
Loading