componentized/directory

componentized:http

interface

A collection of utility components that remix wasi:http types and interfaces.

Interfaces

  • client since 0.1.0
  • latch since 0.1.0 — Access control for wasi:http.
  • status-codes since 0.1.0 — HTTP status codes from the IANA HTTP Status Code Registry, https://www.iana.org/assignments/http-status-codes/

Worlds

WIT

package componentized:http@0.1.0;

@since(version = 0.1.0)
interface client {
  @since(version = 0.1.0)
  enum method {
    get,
    post,
    put,
    delete,
    patch,
    head,
    options,
    trace,
    query,
  }

  /// An error reading the trailers of a response, after its body.
  @since(version = 0.1.0)
  variant trailers-error-code {
    other(option<string>),
  }

  /// Per-request options. None fields fall through to host defaults.
  @since(version = 0.1.0)
  record request-options {
    connect-timeout-ms: option<u32>,
    first-byte-timeout-ms: option<u32>,
    between-bytes-timeout-ms: option<u32>,
    /// Limit the maximum number of redirects to follow for a single
    /// request. 0 disables redirects entirely, the redirect response is
    /// returned. When none, the implementation chooses the limit.
    /// Exceeding the limit is a `redirect-limit-exceeded` error.
    max-redirects: option<u8>,
  }

  /// The request to send to follow a redirect.
  @since(version = 0.1.0)
  record redirect-request {
    method: method,
    url: string,
    /// The request headers, without credentials when the redirect leaves
    /// the origin.
    headers: list<tuple<string, string>>,
    /// The options of the original request, with `max-redirects` set to
    /// the number of redirects remaining. Always set, an option to match
    /// the `options` parameter of the request functions.
    options: option<request-options>,
  }

  /// A streaming HTTP response. The status and headers are available
  /// immediately; the body streams as `body`, and trailers (if any) resolve
  /// via `trailers` once the body stream is fully consumed.
  @since(version = 0.1.0)
  record http-response {
    status: u16,
    headers: list<tuple<string, string>>,
    body: stream<u8>,
    trailers: future<result<list<tuple<string, string>>, trailers-error-code>>,
  }

  @since(version = 0.1.0)
  variant error-code {
    /// The response was a redirect past `request-options.max-redirects`.
    /// Contains the redirect response, and the number of redirects
    /// followed before it.
    redirect-limit-exceeded(tuple<http-response, u8>),
    /// The response was a redirect that sends the request body again. The
    /// body was already streamed, it can't be sent again. Contains the
    /// redirect response, and the request to send with the body to follow
    /// the redirect.
    redirect-requires-body(tuple<http-response, redirect-request>),
    other(option<string>),
  }

  /// Send an HTTP request with an explicit method. Both the request body and
  /// the response body stream.
  @since(version = 0.1.0)
  request: async func(method: method, url: string, headers: list<tuple<string, string>>, body: option<stream<u8>>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP GET request.
  @since(version = 0.1.0)
  get: async func(url: string, headers: list<tuple<string, string>>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP POST request.
  @since(version = 0.1.0)
  post: async func(url: string, headers: list<tuple<string, string>>, body: stream<u8>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP PUT request.
  @since(version = 0.1.0)
  put: async func(url: string, headers: list<tuple<string, string>>, body: stream<u8>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP DELETE request.
  @since(version = 0.1.0)
  delete: async func(url: string, headers: list<tuple<string, string>>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP PATCH request.
  @since(version = 0.1.0)
  patch: async func(url: string, headers: list<tuple<string, string>>, body: stream<u8>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP HEAD request.
  @since(version = 0.1.0)
  head: async func(url: string, headers: list<tuple<string, string>>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP OPTIONS request.
  @since(version = 0.1.0)
  options: async func(url: string, headers: list<tuple<string, string>>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP TRACE request.
  @since(version = 0.1.0)
  trace: async func(url: string, headers: list<tuple<string, string>>, options: option<request-options>) -> result<http-response, error-code>;

  /// HTTP QUERY request.
  @since(version = 0.1.0)
  query: async func(url: string, headers: list<tuple<string, string>>, body: stream<u8>, options: option<request-options>) -> result<http-response, error-code>;
}

/// Access control for wasi:http.
///
/// A gate consults a latch before each request it sends or handles. The latch either defers,
/// raising no objection, or denies the request. A latch never grants a request, a request
/// proceeds when the latch defers. Several latches can be aggregated into one, the request is
/// denied when any of them denies it.
///
/// Deciding and acting on a decision are separate steps. `authorize` decides without side
/// effects, then the gate reports the final decision to `observe-decision`, where a latch updates
/// any state it keeps. A latch aggregated with others may not be asked to authorize a request
/// another latch already denied, but it always observes the final decision.
@since(version = 0.1.0)
interface latch {
  use wasi:http/types@0.3.0.{error-code as http-error-code, request};

  /// Arguments to wasi:http/client#send.
  @since(version = 0.1.0)
  record send-args {
    /// The request to send. A latch may read it, e.g. its method, scheme, authority, path or
    /// headers, but the request is still sent by the gate, after the latch defers.
    request: borrow<request>,
  }

  /// Arguments to wasi:http/handler#handle.
  @since(version = 0.1.0)
  record handle-args {
    /// The request to handle. A latch may read it, e.g. its method, scheme, authority, path or
    /// headers, but the request is still handled by the gate, after the latch defers.
    request: borrow<request>,
  }

  /// Operations of the wasi:http/client interface.
  @since(version = 0.1.0)
  variant client-operation {
    /// Send a request, wasi:http/client#send.
    send(send-args),
  }

  /// Operations of the wasi:http/handler interface.
  @since(version = 0.1.0)
  variant handler-operation {
    /// Handle a request, wasi:http/handler#handle.
    handle(handle-args),
  }

  /// Operations of the wasi:http package a gate asks a latch to authorize.
  @since(version = 0.1.0)
  variant operation {
    /// A request sent with wasi:http/client.
    client(client-operation),
    /// A request handled with wasi:http/handler.
    handler(handler-operation),
  }

  /// Whether the request is allowed to proceed.
  @since(version = 0.1.0)
  variant decision {
    /// The latch raises no objection, the request proceeds unless another latch denies it.
    deferred,
    /// The request must not proceed. The gate fails it with the error code, without sending
    /// or handling it.
    denied(http-error-code),
  }

  /// The name of a latch component, identifying which latch reported an error.
  @since(version = 0.1.0)
  type latch-name = string;

  /// A latch was unable to decide or observe. The gate fails the request.
  @since(version = 0.1.0)
  variant error-code {
    /// The latch's configuration is invalid, the cause is logged by the latch.
    invalid-config(latch-name),
    /// The latch was unable to act on the final decision.
    observation-failed(latch-name),
    /// Any other error.
    other(option<string>),
  }

  /// Decide whether the request may proceed.
  ///
  /// Must not have side effects. When latches are aggregated, a latch is not asked about a
  /// request another latch already denied, so state changed here could disagree with what
  /// actually happened. Act on decisions in `observe-decision` instead.
  @since(version = 0.1.0)
  authorize: func(operation: operation) -> result<decision, error-code>;

  /// Observe the final decision for a request, after `authorize` returned a decision.
  ///
  /// Called for both deferred and denied requests, including requests this latch was not asked
  /// to authorize because an aggregated latch denied them first. The final decision is what the
  /// gate enforces, so this is where a latch updates any state it keeps. Not called when
  /// `authorize` returned an error.
  ///
  /// An error fails the request, since the latch could not act on the decision.
  @since(version = 0.1.0)
  observe-decision: func(final-decision: decision, operation: operation) -> result<_, error-code>;
}

/// HTTP status codes from the IANA HTTP Status Code Registry,
/// https://www.iana.org/assignments/http-status-codes/
@since(version = 0.1.0)
interface status-codes {
  /// 100 Continue
  /// [RFC9110, Section 15.2.1]
  /// @value 100
  continue: func() -> u16;

  /// 101 Switching Protocols
  /// [RFC9110, Section 15.2.2]
  /// @value 101
  switching-protocols: func() -> u16;

  /// 102 Processing
  /// [RFC2518]
  /// @value 102
  processing: func() -> u16;

  /// 103 Early Hints
  /// [RFC8297]
  /// @value 103
  early-hints: func() -> u16;

  /// 200 OK
  /// [RFC9110, Section 15.3.1]
  /// @value 200
  ok: func() -> u16;

  /// 201 Created
  /// [RFC9110, Section 15.3.2]
  /// @value 201
  created: func() -> u16;

  /// 202 Accepted
  /// [RFC9110, Section 15.3.3]
  /// @value 202
  accepted: func() -> u16;

  /// 203 Non-Authoritative Information
  /// [RFC9110, Section 15.3.4]
  /// @value 203
  non-authoritative-information: func() -> u16;

  /// 204 No Content
  /// [RFC9110, Section 15.3.5]
  /// @value 204
  no-content: func() -> u16;

  /// 205 Reset Content
  /// [RFC9110, Section 15.3.6]
  /// @value 205
  reset-content: func() -> u16;

  /// 206 Partial Content
  /// [RFC9110, Section 15.3.7]
  /// @value 206
  partial-content: func() -> u16;

  /// 207 Multi-Status
  /// [RFC4918]
  /// @value 207
  multi-status: func() -> u16;

  /// 208 Already Reported
  /// [RFC5842]
  /// @value 208
  already-reported: func() -> u16;

  /// 226 IM Used
  /// [RFC3229]
  /// @value 226
  im-used: func() -> u16;

  /// 300 Multiple Choices
  /// [RFC9110, Section 15.4.1]
  /// @value 300
  multiple-choices: func() -> u16;

  /// 301 Moved Permanently
  /// [RFC9110, Section 15.4.2]
  /// @value 301
  moved-permanently: func() -> u16;

  /// 302 Found
  /// [RFC9110, Section 15.4.3]
  /// @value 302
  found: func() -> u16;

  /// 303 See Other
  /// [RFC9110, Section 15.4.4]
  /// @value 303
  see-other: func() -> u16;

  /// 304 Not Modified
  /// [RFC9110, Section 15.4.5]
  /// @value 304
  not-modified: func() -> u16;

  /// 305 Use Proxy
  /// [RFC9110, Section 15.4.6]
  /// @value 305
  use-proxy: func() -> u16;

  /// 307 Temporary Redirect
  /// [RFC9110, Section 15.4.8]
  /// @value 307
  temporary-redirect: func() -> u16;

  /// 308 Permanent Redirect
  /// [RFC9110, Section 15.4.9]
  /// @value 308
  permanent-redirect: func() -> u16;

  /// 400 Bad Request
  /// [RFC9110, Section 15.5.1]
  /// @value 400
  bad-request: func() -> u16;

  /// 401 Unauthorized
  /// [RFC9110, Section 15.5.2]
  /// @value 401
  unauthorized: func() -> u16;

  /// 402 Payment Required
  /// [RFC9110, Section 15.5.3]
  /// @value 402
  payment-required: func() -> u16;

  /// 403 Forbidden
  /// [RFC9110, Section 15.5.4]
  /// @value 403
  forbidden: func() -> u16;

  /// 404 Not Found
  /// [RFC9110, Section 15.5.5]
  /// @value 404
  not-found: func() -> u16;

  /// 405 Method Not Allowed
  /// [RFC9110, Section 15.5.6]
  /// @value 405
  method-not-allowed: func() -> u16;

  /// 406 Not Acceptable
  /// [RFC9110, Section 15.5.7]
  /// @value 406
  not-acceptable: func() -> u16;

  /// 407 Proxy Authentication Required
  /// [RFC9110, Section 15.5.8]
  /// @value 407
  proxy-authentication-required: func() -> u16;

  /// 408 Request Timeout
  /// [RFC9110, Section 15.5.9]
  /// @value 408
  request-timeout: func() -> u16;

  /// 409 Conflict
  /// [RFC9110, Section 15.5.10]
  /// @value 409
  conflict: func() -> u16;

  /// 410 Gone
  /// [RFC9110, Section 15.5.11]
  /// @value 410
  gone: func() -> u16;

  /// 411 Length Required
  /// [RFC9110, Section 15.5.12]
  /// @value 411
  length-required: func() -> u16;

  /// 412 Precondition Failed
  /// [RFC9110, Section 15.5.13]
  /// @value 412
  precondition-failed: func() -> u16;

  /// 413 Content Too Large
  /// [RFC9110, Section 15.5.14]
  /// @value 413
  content-too-large: func() -> u16;

  /// 414 URI Too Long
  /// [RFC9110, Section 15.5.15]
  /// @value 414
  uri-too-long: func() -> u16;

  /// 415 Unsupported Media Type
  /// [RFC9110, Section 15.5.16]
  /// @value 415
  unsupported-media-type: func() -> u16;

  /// 416 Range Not Satisfiable
  /// [RFC9110, Section 15.5.17]
  /// @value 416
  range-not-satisfiable: func() -> u16;

  /// 417 Expectation Failed
  /// [RFC9110, Section 15.5.18]
  /// @value 417
  expectation-failed: func() -> u16;

  /// 418 I'm a teapot, unused by HTTP and reserved so it isn't assigned
  /// [RFC2324], [RFC9110, Section 15.5.19]
  /// @value 418
  im-a-teapot: func() -> u16;

  /// 421 Misdirected Request
  /// [RFC9110, Section 15.5.20]
  /// @value 421
  misdirected-request: func() -> u16;

  /// 422 Unprocessable Content
  /// [RFC9110, Section 15.5.21]
  /// @value 422
  unprocessable-content: func() -> u16;

  /// 423 Locked
  /// [RFC4918]
  /// @value 423
  locked: func() -> u16;

  /// 424 Failed Dependency
  /// [RFC4918]
  /// @value 424
  failed-dependency: func() -> u16;

  /// 425 Too Early
  /// [RFC8470]
  /// @value 425
  too-early: func() -> u16;

  /// 426 Upgrade Required
  /// [RFC9110, Section 15.5.22]
  /// @value 426
  upgrade-required: func() -> u16;

  /// 428 Precondition Required
  /// [RFC6585]
  /// @value 428
  precondition-required: func() -> u16;

  /// 429 Too Many Requests
  /// [RFC6585]
  /// @value 429
  too-many-requests: func() -> u16;

  /// 431 Request Header Fields Too Large
  /// [RFC6585]
  /// @value 431
  request-header-fields-too-large: func() -> u16;

  /// 451 Unavailable For Legal Reasons
  /// [RFC7725]
  /// @value 451
  unavailable-for-legal-reasons: func() -> u16;

  /// 500 Internal Server Error
  /// [RFC9110, Section 15.6.1]
  /// @value 500
  internal-server-error: func() -> u16;

  /// 501 Not Implemented
  /// [RFC9110, Section 15.6.2]
  /// @value 501
  not-implemented: func() -> u16;

  /// 502 Bad Gateway
  /// [RFC9110, Section 15.6.3]
  /// @value 502
  bad-gateway: func() -> u16;

  /// 503 Service Unavailable
  /// [RFC9110, Section 15.6.4]
  /// @value 503
  service-unavailable: func() -> u16;

  /// 504 Gateway Timeout
  /// [RFC9110, Section 15.6.5]
  /// @value 504
  gateway-timeout: func() -> u16;

  /// 505 HTTP Version Not Supported
  /// [RFC9110, Section 15.6.6]
  /// @value 505
  http-version-not-supported: func() -> u16;

  /// 506 Variant Also Negotiates
  /// [RFC2295]
  /// @value 506
  variant-also-negotiates: func() -> u16;

  /// 507 Insufficient Storage
  /// [RFC4918]
  /// @value 507
  insufficient-storage: func() -> u16;

  /// 508 Loop Detected
  /// [RFC5842]
  /// @value 508
  loop-detected: func() -> u16;

  /// 510 Not Extended, obsoleted
  /// [RFC2774], [Status change of HTTP experiments to Historic]
  /// @value 510
  not-extended: func() -> u16;

  /// 511 Network Authentication Required
  /// [RFC6585]
  /// @value 511
  network-authentication-required: func() -> u16;
}

world imports {
  import client;
  import wasi:clocks/types@0.3.0;
  import wasi:http/types@0.3.0;
  import latch;
}