HTTP

HTTP client and server APIs in Tish (feature flag: http).

Requires the http feature. Build tish with it for run: cargo run -p tishlang --features http -- run main.tish. For build: tish build main.tish -o app --feature http.

fetch(url, options?)

Perform an HTTP request. fetch returns a Promise that settles to a response object. Always await the result (or use .then) — there is no synchronous “already materialized” response.

import { fetch } from "http"
 
let res = await fetch("https://api.example.com/data")
console.log(res.status, res.ok)

Parameters

  • url (string): URL to fetch
  • options (object, optional):
    • method: "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"
    • headers: Object of header name → value
    • body: Request body string

Response object (client fetch)

  • status: Status code (number)
  • ok: Boolean (true if 2xx)
  • headers: Object of header name → value
  • body: Opaque ReadableStream (not a string). Use body.getReader() and a read loop, or consume the whole body with await res.text() / await res.json() (see below).
  • text: Function with no args → returns a Promise<string> that reads the entire body as UTF-8 text.
  • json: Function with no args → returns a Promise that reads the entire body and JSON.parses it.

Buffered JSON / text (small responses)

For typical JSON APIs, read the full body in one step:

import { fetch } from "http"
 
let res = await fetch("https://api.example.com/json")
if (res.ok) {
  let data = await res.json()
  console.log(data)
}
 
let res2 = await fetch("https://api.example.com/plain")
let txt = await res2.text()

ReadableStream and getReader()

The response body is a Web-fetch–style readable stream:

  1. Call let reader = res.body.getReader() (locks the body for streaming).
  2. Loop: let chunk = await reader.read().
  3. Each chunk is { done: boolean, value: number[] }: value is the next slice of raw bytes (UTF-8 is not decoded for you; merge and decode as needed).
  4. Stop when done is true.

Single-consumer rule: After getReader(), do not call await res.text() or await res.json() on the same response — the body can only be consumed one way. (Same idea as browsers: locked stream vs. fully buffered helpers.)

Streaming example (accumulate bytes / line-based protocols)

This pattern matches incremental SSE or other protocols where you decode UTF-8 and split frames yourself:

import { fetch } from "http"
 
let res = await fetch("https://example.com/stream", { method: "GET" })
if (!res.ok) {
  console.log("error", res.status)
} else {
  let reader = res.body.getReader()
  let acc = []
  while (true) {
    let chunk = await reader.read()
    if (chunk.done) {
      break
    }
    let bytes = chunk.value
    let i = 0
    while (i < bytes.length) {
      acc.push(bytes[i])
      i = i + 1
    }
  }
  // acc is number[] of UTF-8 bytes; decode to string in your app if needed
}

The Tish runtime integration test fetch_readable_stream.rs (in the tish repo) exercises chunked bodies end-to-end.

fetchAll(requests)

fetchAll returns a Promise that settles to an array of response objects (same shape as fetch above). Use await fetchAll([...]).

Each request is a string URL or an object with url (and optional method, headers, body).

import { fetch, fetchAll } from "http"
 
let results = await fetchAll([
  "https://api.example.com/a",
  { url: "https://api.example.com/b", method: "POST" },
])

serve(port, handler)

Start an HTTP server. The handler receives a request object and returns a response object or string.

Request object

  • method: HTTP method (string)
  • path: Path without query (string)
  • url: Full URL (string)
  • query: Query string (string)
  • headers: Object of header name → value
  • body: Request body (string on the server)

Response

Return an object:

  • status: Status code (number, default 200)
  • body: Response body (string)
  • headers: Object of header name → value (optional, e.g. { contentType: "application/json" })

Or return a string (200, no headers).

Server-side body as string is intentional and not the same shape as client fetch res.body (opaque byte stream).

Example

fn handleRequest(req) {
  if (req.path === "/health") {
    return { status: 200, body: "OK" }
  }
  if (req.path === "/") {
    return {
      status: 200,
      headers: { contentType: "application/json" },
      body: JSON.stringify({ message: "Hello" }),
    }
  }
  return { status: 404, body: "Not Found" }
}
 
let port = "PORT" in process.env ? parseInt(process.env.PORT) : 8080
serve(port, handleRequest)

Promise and timers

With the http feature, Tish also provides:

  • Promise (ECMA-262 §27.2): Promise(executor), .then, .catch, .finally, Promise.resolve, Promise.reject, Promise.all, Promise.race
  • setTimeout(callback, delayMs, ...args) — Schedule callback to run after delay (non-blocking; returns immediately)
  • setInterval(callback, delayMs, ...args) — Schedule callback to run repeatedly (non-blocking)
  • clearTimeout(id), clearInterval(id) — Cancel timers

Use async fn / await for HTTP and other Promise-based APIs.

Improve this documentation