Skip to content
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

Document error codes. #18

Merged
merged 11 commits into from
Mar 13, 2023
522 changes: 405 additions & 117 deletions example-world.md

Large diffs are not rendered by default.

36 changes: 23 additions & 13 deletions wit/ip-name-lookup.wit
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@

default interface ip-name-lookup {
use poll.poll.{pollable}
use pkg.network.{network, error, ip-address, ip-address-family}
use pkg.network.{network, error-code, ip-address, ip-address-family}


/// Resolve an internet host name to a list of IP addresses.
///
/// See the wasi-socket proposal README.md for a comparison with getaddrinfo.
///
/// Parameters:
/// # Parameters
/// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted
/// to ASCII using IDNA encoding.
/// - `address-family`: If provided, limit the results to addresses of this specific address family.
Expand All @@ -18,18 +18,23 @@ default interface ip-name-lookup {
/// - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address.
/// - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged.
///
/// This function never blocks. It either immediately returns successfully with a `resolve-address-stream`
/// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream`
/// that can be used to (asynchronously) fetch the results.
/// Or it immediately fails whenever `name` is:
/// - empty
/// - an IP address
/// - a syntactically invalid domain name in another way
///
/// References:
/// At the moment, the stream never completes successfully with 0 items. Ie. the first call
/// to `resolve-next-address` never returns `ok(none)`. This may change in the future.
///
/// # Typical errors
/// - `invalid-name`: `name` is a syntactically invalid domain name.
/// - `invalid-name`: `name` is an IP address.
/// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY)
///
/// # References:
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html>
/// - <https://man7.org/linux/man-pages/man3/getaddrinfo.3.html>
///
resolve-addresses: func(network: network, name: string, address-family: option<ip-address-family>, include-unavailable: bool) -> result<resolve-address-stream, error>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo>
/// - <https://man.freebsd.org/cgi/man.cgi?query=getaddrinfo&sektion=3>
resolve-addresses: func(network: network, name: string, address-family: option<ip-address-family>, include-unavailable: bool) -> result<resolve-address-stream, error-code>



Expand All @@ -43,7 +48,12 @@ default interface ip-name-lookup {
/// After which, you should release the stream with `drop-resolve-address-stream`.
///
/// This function never returns IPv4-mapped IPv6 addresses.
resolve-next-address: func(this: resolve-address-stream) -> result<option<ip-address>, error>
///
/// # Typical errors
/// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY)
/// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN)
/// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL)
resolve-next-address: func(this: resolve-address-stream) -> result<option<ip-address>, error-code>



Expand All @@ -60,8 +70,8 @@ default interface ip-name-lookup {
///
/// Note: these functions are here for WASI Preview2 only.
/// They're planned to be removed when `future` is natively supported in Preview3.
non-blocking: func(this: resolve-address-stream) -> result<bool, error>
set-non-blocking: func(this: resolve-address-stream, value: bool) -> result<_, error>
non-blocking: func(this: resolve-address-stream) -> result<bool, error-code>
set-non-blocking: func(this: resolve-address-stream, value: bool) -> result<_, error-code>

/// Create a `pollable` which will resolve once the stream is ready for I/O.
///
Expand Down
124 changes: 121 additions & 3 deletions wit/network.wit
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,129 @@ default interface network {
drop-network: func(this: network)


/// Error codes.
///
/// In theory, every API can return any error code.
/// In practice, API's typically only return the errors documented per API
/// combined with a couple of errors that are always possible:
/// - `unknown`
/// - `access-denied`
/// - `not-supported`
/// - `out-of-memory`
///
/// See each individual API for what the POSIX equivalents are. They sometimes differ per API.
enum error-code {
// ### GENERAL ERRORS ###

enum error {
/// Unknown error
unknown,
again,
// TODO ...

/// Access denied.
///
/// POSIX equivalent: EACCES, EPERM
access-denied,

/// The operation is not supported.
///
/// POSIX equivalent: EOPNOTSUPP
not-supported,

/// Not enough memory to complete the operation.
///
/// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY
out-of-memory,

/// The operation timed out before it could finish completely.
timeout,

/// This operation is incompatible with another asynchronous operation that is already in progress.
concurrency-conflict,
sunfishcode marked this conversation as resolved.
Show resolved Hide resolved


// ### IP ERRORS ###

/// The specified address-family is not supported.
address-family-not-supported,

/// An IPv4 address was passed to an IPv6 resource, or vice versa.
address-family-mismatch,

/// The socket address is not a valid remote address. E.g. the IP address is set to INADDR_ANY, or the port is set to 0.
invalid-remote-address,

/// The operation is only supported on IPv4 resources.
ipv4-only-operation,

/// The operation is only supported on IPv6 resources.
ipv6-only-operation,



// ### TCP & UDP SOCKET ERRORS ###

/// A new socket resource could not be created because of a system limit.
new-socket-limit,

/// The socket is already attached to another network.
already-attached,

/// The socket is already bound.
already-bound,

/// The socket is already in the Connection state.
already-connected,

/// The socket is not bound to any local address.
not-bound,

/// The socket is not in the Connection state.
not-connected,

/// A bind operation failed because the provided address is not an address that the `network` can bind to.
address-not-bindable,

/// A bind operation failed because the provided address is already in use.
address-in-use,

/// A bind operation failed because there are no ephemeral ports available.
ephemeral-ports-exhausted,

/// The remote address is not reachable
remote-unreachable,


// ### TCP SOCKET ERRORS ###

/// The socket is already in the Listener state.
already-listening,

/// The socket is already in the Listener state.
not-listening,

/// The connection was forcefully rejected
connection-refused,

/// The connection was reset.
connection-reset,
Copy link
Member

Choose a reason for hiding this comment

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

Should there be a code corresponding to ECONNABORTED?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

AFAIK, ECONNABORTED is only returned by accept. Which, in my opinion should not be forwarded into WASI land. This is documented further along in the document:

Host implementations must skip over transient errors returned by the native accept syscall.

Copy link
Member

Choose a reason for hiding this comment

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

This is a little surprising. Indeed many users will want to just ignore ECONNABORTED and keep waiting for the next connection, but some users may want to know that the failure occurred so that they can log it. Large numbers of reset connections could be a sign of a network problem or unexpected client behavior. Some users may expect specific connections and want to know that a connection was attempted and reset so that they don't continue to wait for it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The choice of skipping transient errors stems from before the Preview2 PR. Before that, accept returned a single stream of TCP sockets, that would only close after a permanent error (if ever). This is a design avenue I'd still like to explore for Preview3.

As you noted, there are indeed valid use cases for receiving these transient errors. It just felt too niche to make it into the first version of the proposal.

I think it can even be added afterwards, without breaking the current setup, like this:

/// Returns a notification stream of network errors.
/// Can maybe even be generalized to include OS-specific diagnostic options like IP_RECVERR. (haven't researched this at all)
get-errors(this: tcp-socket) -> stream<error, _>

then the libc implementation of accept can call poll-oneof([accept-stream, error-stream]).

Copy link
Member

Choose a reason for hiding this comment

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

I can see the appeal of a design like get-errors, but worry it would create awkward new surface area. How would it work if the user polls on just the error-stream and not the accept-stream? The underlying host APIs don't have any way to report ECONNABORTED without actually trying to accept a connection.

However, I can see the logic of deferring this for now. I've filed #22 to track this, and we can proceed with this PR.



// ### UDP SOCKET ERRORS ###
datagram-too-large,


// ### NAME LOOKUP ERRORS ###

/// The provided name is a syntactically invalid domain name.
invalid-name,

/// Name does not exist or has no suitable associated IP addresses.
name-unresolvable,

/// A temporary failure in name resolution occurred.
temporary-resolver-failure,

/// A permanent failure in name resolution occurred.
permanent-resolver-failure,
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to move resolver errors to a separate enum? At a glance, the only code they share with the socket errors is out-of-memory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They also share:

  • address-family-not-supported
  • access-denied
  • not-supported
  • resource-limit-reached

Copy link
Member

Choose a reason for hiding this comment

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

When would a resolver API return access-denied? When would it return not-supported?

Copy link
Collaborator Author

@badeend badeend Feb 28, 2023

Choose a reason for hiding this comment

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

Examples:

  • access-denied: The network does not allow you to look up the specific name.
  • not-supported: The network does not support name resolution.

I don't know if operation systems ever return these error codes natively.
But in WASI, with its increased focus on sandbox-ability and virtualization, it makes sense to me to give embedders an escape hatch in case they don't want to support/give access to a specific function in an interface. This applies in general, not just the resolve API.

}

enum ip-address-family {
Expand Down
14 changes: 10 additions & 4 deletions wit/tcp-create-socket.wit
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

default interface tcp-create-socket {
use pkg.network.{network, error, ip-address-family}
use pkg.network.{network, error-code, ip-address-family}
use pkg.tcp.{tcp-socket}

/// Create a new TCP socket.
Expand All @@ -11,9 +11,15 @@ default interface tcp-create-socket {
/// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`listen`/`connect`
/// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world.
///
/// References:
/// # Typical errors
/// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP)
/// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT)
/// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html>
/// - <https://man7.org/linux/man-pages/man2/socket.2.html>
///
create-tcp-socket: func(address-family: ip-address-family) -> result<tcp-socket, error>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw>
/// - <https://man.freebsd.org/cgi/man.cgi?query=socket&sektion=2>
create-tcp-socket: func(address-family: ip-address-family) -> result<tcp-socket, error-code>
}
Loading