@std/web

API Reference

class Headers

Case-insensitive multi-value header table preserving insertion order. HTTP header names are case-insensitive, so every name is folded to lowercase on the way in: set("Content-Type", ...) and get("content-type") touch the same entry. One name holds one value; a repeated name overwrites, so this is not a multi-map like URLSearchParams. New headers start empty, and keys() lists them in the order they were first stored.

init()

fields

  • table: Map<String, String>
fn clear()

Drop every entry at once. The table stays usable, so a cleared header table can be filled again from scratch.

import { Headers } from "@std/web";

let h = Headers.fromPairs([["A", "1"], ["B", "2"]]);
h.clear();
print(h.len(), h.keys().length);
Run in Playground
fn delete(name: String): Bool

Remove a header.

name — header name; case does not matter.

returns — true when an entry was removed, false when there was none.

import { Headers } from "@std/web";

let h = Headers.fromPairs([["X-Key", "v"]]);
print(h.delete("x-key"), h.len());
print(h.delete("x-key"));
Run in Playground
fn fromMap(m: Map<String, String>): Headers

Build from a Map of name to value. Names go through the same lowercase fold as set(), and the Map's own order is kept.

m — source entries.

returns — header table copy.

import { Headers } from "@std/web";
import { Map } from "@std/collections";

let m = new Map<String, String>();
m.set("Accept", "text/plain");
m.set("X-Trace", "abc");
let h = Headers.fromMap(m);
print(h.len(), h.get("accept") ?? "");
Run in Playground
fn fromPairs(pairs: Array<Array<String>>): Headers

Build from [name, value] pairs; short pairs are skipped. Only the first two elements of each pair are read, so extra elements are ignored rather than treated as an error.

pairs — name/value arrays.

returns — header table copy.

import { Headers } from "@std/web";

let h = Headers.fromPairs([["Content-Type", "text/plain"], ["X-N", "1"]]);
print(h.get("content-type") ?? "", h.len());
Run in Playground
fn get(name: String): String?

Read a header by case-insensitive name.

name — header name; case does not matter.

returns — value, or `null` when missing. A header set to the empty string gives `""`, not `null`.

import { Headers } from "@std/web";

let h = new Headers();
h.set("Content-Type", "text/plain");
print(h.get("CONTENT-TYPE") ?? "none", h.get("missing") ?? "none");
Run in Playground
fn has(name: String): Bool

Membership test by case-insensitive name.

name — header name.

returns — true when set and not deleted.

import { Headers } from "@std/web";

let h = new Headers();
h.set("X-Key", "v");
print(h.has("x-key"), h.has("X-Other"));
Run in Playground
fn keys(): Array<String>

All names in insertion order, lowercased as stored.

returns — name array copy with typed entries.

import { Headers } from "@std/web";

let h = Headers.fromPairs([["Content-Type", "text/plain"], ["X-N", "1"]]);
print(h.keys().join(","));
Run in Playground
fn len(): Int

Number of live entries.

returns — entry count. Names differing only in case count once.

import { Headers } from "@std/web";

let h = Headers.fromPairs([["A", "1"], ["B", "2"]]);
print(h.len());
h.clear();
print(h.len());
Run in Playground
fn set(name: String, value: String)

Store a header, overwriting any previous value. The name is folded to lowercase, and a repeated name keeps its original position in keys().

name — header name.

value — header value.

import { Headers } from "@std/web";

let h = new Headers();
h.set("Accept", "text/plain");
h.set("accept", "text/html");
print(h.len(), h.get("Accept") ?? "");
Run in Playground
fn values(): Array<String>

All values in name insertion order.

returns — value array copy aligned with keys(), so `keys()[i]` and `values()[i]` are the same header.

import { Headers } from "@std/web";

let h = Headers.fromPairs([["A", "1"], ["B", "2"]]);
print(h.values().join(","));
print(h.keys()[0], h.values()[0]);
Run in Playground
class RequestOptions

Per-request knobs for fetch(). Pass an instance as the options argument: method override, extra request headers, and an optional ASCII body.

The three fields are public, so set them directly and the accessors read them back. A new instance means GET with no headers and an empty body. Header names go through the same lowercase fold as Headers, and fetch() writes them after its own Host, Connection, User-Agent, and Accept lines, so a name repeated here is sent twice.

The method string is sent as written, so use the uppercase form the server expects.

init(method: String)

fields

  • body: String
  • headers: Headers
  • method: String
fn bodyText(): String

Request body text.

returns — body string, "" when none was set. fetch() adds `Content-Length` from this length and skips the header when the body is empty.

import { RequestOptions } from "@std/web";

let o = new RequestOptions("POST");
o.body = "name=ada";
print(o.bodyText().length);
Run in Playground
fn extraHeaders(): Headers

Extra request headers.

returns — live header table, not a copy: setting a header here after building the options still affects the next fetch().

import { RequestOptions } from "@std/web";

let o = new RequestOptions();
o.extraHeaders().set("Accept-Language", "en");
print(o.extraHeaders().len());
Run in Playground
fn methodName(): String

HTTP method to send.

returns — method string exactly as set, with no uppercasing.

import { RequestOptions } from "@std/web";

let o = new RequestOptions("PATCH");
print(o.methodName());
Run in Playground
class Response

HTTP response holder returned by fetch().

The whole body is read before this is built and stored as text, so there is no streaming and no body stream to consume. A non-2xx status is a normal return, not a failure: check status or isSuccess() yourself and decide what to do.

Construct one directly to exercise code that consumes responses without a server: supply the status, the reason phrase from the status line, the header table, and the body text.

init(status: Int, statusText: String, headers: Headers, body: String)

fields

  • body: String
  • headers: Headers
  • status: Int
  • statusText: String
fn json(): Any

Body parsed as JSON. Numbers without a fraction decode to Int, the rest to Float.

returns — decoded body value as `Any`; aborts with a parse offset when the body is not valid JSON. Check `status` and `text()` first if the body might not be JSON, since a parse failure is a fault and not a catchable error value.

import { Response, Headers } from "@std/web";
import { JSON } from "@std/json";

let r = new Response(200, "OK", new Headers(), "\{\"n\":7,\"ok\":true}");
print(JSON.stringify(r.json()));
Run in Playground
fn text(): String

Body as text. Same string as the body field; the body is already decoded, so this does no charset conversion.

returns — response body string.

import { Response, Headers } from "@std/web";

print(new Response(200, "OK", new Headers(), "hello").text());
Run in Playground
class URL

WHATWG-subset URL: absolute parse plus relative resolution. No IPv6 literals, no credentials, no IDNA: hosts fold ASCII case only.

Construct with an absolute URL, or with a relative reference plus an absolute base. The reference forms follow RFC 3986: //host/path borrows the base scheme, /path replaces the path, ?query keeps the path, #frag keeps path and query, "" keeps everything, and anything else merges onto the base directory and then removes dot segments. Resolution removes dot segments; parsing an absolute URL does not, so new URL("https://a.com/a/../b").pathname() is /a/../b. Percent escapes are stored as written and never decoded here; use decodeComponent() when you need the decoded text.

Nothing is normalized beyond case folding of the scheme and host, and serialization drops a port that matches the scheme default.

init(url: String, base: String)

fields

  • fragment: String
  • host: String
  • path: String
  • port: String
  • query: String
  • scheme: String
fn hash(): String

Fragment with leading #, "" when absent.

returns — hash string. A URL built from a base drops the base's fragment unless the reference carried one, and a reference that is just `?query` drops it too.

import { URL } from "@std/web";

print(new URL("https://a.com/x#top").hash());
print(new URL("?q=1", "https://a.com/x#top").hash() == "");
Run in Playground
fn host(): String

Host with non-default port.

returns — e.g. "example.com:8080", or just the host when the port is absent or matches the scheme default.

import { URL } from "@std/web";

print(new URL("https://a.com:8443/x").host());
print(new URL("https://a.com:443/x").host());
Run in Playground
fn hostname(): String

Lowercased host without port.

returns — host string, "" when the URL has no authority.

import { URL } from "@std/web";

print(new URL("http://A.com:8080/x").hostname());
Run in Playground
fn href(): String

Full serialized URL, same string as toString().

returns — href string.

import { URL } from "@std/web";

let u = new URL("https://example.com:8080/a/b?x=1#frag");
print(u.href());
print(u.hostname(), u.port(), u.pathname());
Run in Playground
fn origin(): String

Scheme, host, and non-default port.

returns — origin string, or "null" for relative URLs. A URL without a host, such as `mailto:[email protected]`, also gives "null": the result is the string `"null"`, not a null value.

import { URL } from "@std/web";

print(new URL("https://a.com:8443/x").origin());
print(new URL("/rel").origin());
Run in Playground
fn pathname(): String

Raw (still-encoded) path.

returns — path string, "" for a relative reference with no path and "/" for a hosted URL with an empty path.

import { URL } from "@std/web";

print(new URL("https://a.com/a/b%20c").pathname());
Run in Playground
fn port(): String

Port digits, "" when absent. This is the port as written, with no default filled in: https://a.com/x reports "", not 443. Use defaultPort() to add the scheme default yourself.

returns — port string.

import { URL, defaultPort } from "@std/web";

let u = new URL("https://a.com/x");
print(u.port() == "", defaultPort(u.scheme));
Run in Playground
fn protocol(): String

Scheme with trailing colon.

returns — e.g. "https:" or "" for relative URLs.

import { URL } from "@std/web";

print(new URL("https://a.com/").protocol());
print(new URL("/rel").protocol() == "");
Run in Playground
fn search(): String

Query with leading ?, "" when absent.

returns — search string, percent-encoding untouched.

import { URL } from "@std/web";

print(new URL("https://a.com/?x=1&y=2").search());
Run in Playground
fn searchParams(): URLSearchParams

Query parsed into parameters (snapshot, not live). Names and values are percent-decoded with + read as a space. The returned table is a copy: changing it does not change the URL, and changing the URL does not change an earlier table. Write results back with setSearch().

returns — parameter table.

import { URL } from "@std/web";

let u = new URL("https://a.com/?q=a+b&tag=x%20y");
let p = u.searchParams();
print(p.get("q") ?? "", p.get("tag") ?? "");
p.set("q", "c+d");
u.setSearch(p.toString());
print(u.search());
Run in Playground
fn setHash(h: String)

Replace the fragment; a leading # is stripped. The text is stored as given, so a fragment of "" clears it.

h — new fragment, with or without the leading `#`.

import { URL } from "@std/web";

let u = new URL("https://a.com/x");
u.setHash("#top");
print(u.hash());
u.setHash("");
print(u.href());
Run in Playground
fn setPathname(p: String)

Replace the path; a missing leading slash is added when hosted. An empty path becomes / on a hosted URL and stays empty on a relative one. Dot segments are not removed here: call removeDotSegments() yourself if you need that.

p — new path.

import { URL } from "@std/web";

let u = new URL("https://a.com/old");
u.setPathname("new");
print(u.href());
Run in Playground
fn setSearch(q: String)

Replace the query; a leading ? is stripped. The text is stored as given, with no encoding applied, so passing URLSearchParams.toString() output round-trips.

q — new query, with or without the leading `?`.

import { URL } from "@std/web";

let u = new URL("https://a.com/x?old=1");
u.setSearch("?a=1&b=2");
print(u.href());
Run in Playground
fn toString(): String

Serialize to the href string. A hosted URL prints scheme://host[:port]/path[?query][#frag] with a default port left out and an empty path written as /; a relative URL prints just its path, query, and fragment.

returns — serialized URL.

import { URL } from "@std/web";

print(new URL("https://a.com:443/x").href());
print(new URL("/only/a/path?x=1").href());
Run in Playground
class URLSearchParams

Insertion-ordered query parameter table with form encoding.

Names and values are stored decoded, so a+b and a%20b both read back as "a b"; toString() re-encodes with form rules, which turns a space into + again. Entries are kept in insertion order and a name may repeat, which is how repeated query keys stay distinct. get() returns the first match and getAll() returns every one.

Construct from a query string with a leading ? if you like. To build one from scratch, start empty and use append() or set().

init(query: String)

fields

  • names: Array<String>
  • values: Array<String>
fn append(name: String, value: String)

Append another value for name. Nothing is removed, so a name that already exists gains a second entry. Unlike set(), the order of the other names is untouched.

name — parameter name.

value — parameter value, stored decoded.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("tag=x");
p.append("tag", "y");
print(p.getAll("tag").join(","), p.size());
Run in Playground
fn delete(name: String): Bool

Remove every entry for name, all duplicates included. The remaining names keep their relative order.

name — parameter name.

returns — true when anything was removed.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("a=1&b=2&a=3");
print(p.delete("a"), p.toString());
print(p.size());
Run in Playground
fn entries(): Array<Array<String>>

All [name, value] pairs in order. The pairs are fresh two-element arrays, so changing one does not change the table.

returns — pair array.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("a=1&b=2");
for pair in p.entries() {
print(pair[0], pair[1]);
}
Run in Playground
fn fromMap(m: Map<String, String>): URLSearchParams

Build from a Map of name to value. The Map's order becomes the parameter order, and values are taken as decoded text: a name or value that already contains % or + is stored literally, not decoded again.

m — source entries.

returns — parameter table copy.

import { URLSearchParams } from "@std/web";
import { Map } from "@std/collections";

let m = new Map<String, String>();
m.set("q", "rasmalai");
m.set("page", "2");
print(URLSearchParams.fromMap(m).toString());
Run in Playground
fn get(name: String): String?

First value for name. Names are matched exactly, with no case folding and no decoding applied to the name you pass in.

name — parameter name.

returns — value, or `null` when missing. A name present with an empty value gives `""`, not `null`.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("a=1&a=2&flag");
print(p.get("a") ?? "none", p.get("flag") ?? "none", p.get("zz") ?? "none");
Run in Playground
fn getAll(name: String): Array<String>

Every value for name in order. This is how a repeated query key stays visible: ?tag=a&tag=b gives two entries here. An empty result means the name is absent, not that its value is empty.

name — parameter name.

returns — value array in insertion order, empty when missing.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("a=1&a=2&b=3");
print(p.getAll("a").length(), p.get("b") ?? "");
Run in Playground
fn has(name: String): Bool

Membership test.

name — parameter name.

returns — true when at least one entry exists, even if its value is empty.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("flag&a=1");
print(p.has("flag"), p.has("a"), p.has("b"));
Run in Playground
fn iterator(): Iterator<Array<String>>

Iterate [name, value] pairs in order with for..in. This is what makes URLSearchParams an Iterable, so a for loop over one yields pairs. The iterator walks a snapshot from entries(), so changing the table inside the loop does not affect the walk.

returns — pair iterator.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("x=1&y=2");
for pair in p {
print(pair[0], pair[1]);
}
Run in Playground
fn keys(): Array<String>

All names in order, duplicates kept.

returns — name array copy, aligned with `values()`.

import { URLSearchParams } from "@std/web";

print(new URLSearchParams("a=1&b=2&a=3").keys().join(","));
Run in Playground
fn parse(query: String)

Add every parameter in a query string to the table. This appends; it does not clear what is already there, so calling it twice with the same text gives two copies of every entry.

query — raw query text, with or without the leading `?`.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("a=1");
p.parse("b=2&c");
print(p.size(), p.toString());
Run in Playground
fn set(name: String, value: String)

Set name to a single value, dropping previous entries. The new entry goes to the end of the order, so set() on a repeated name both collapses the duplicates and moves the value to the end.

name — parameter name.

value — parameter value, stored decoded.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("a=1&b=2&a=3");
p.set("a", "9");
print(p.toString());
Run in Playground
fn size(): Int

Number of entries.

returns — entry count, so a repeated name counts once per value.

import { URLSearchParams } from "@std/web";

print(new URLSearchParams("a=1&a=2").size());
Run in Playground
fn sort()

Stable sort by name, keeping duplicate order. Names compare scalar by scalar, so uppercase sorts before lowercase, and entries sharing a name stay in the order they were added.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams("c=3&a=1&a=2");
p.sort();
print(p.toString());
Run in Playground
fn toString(): String

Form-encode entries joined by &. Names and values are percent-encoded with form rules, so a space becomes + and every entry keeps its =, even one with an empty value. An empty table gives "", with no leading ?; add that yourself when building a URL.

returns — encoded query string.

import { URLSearchParams } from "@std/web";

let p = new URLSearchParams();
p.append("q", "a b&c");
print(p.toString());
Run in Playground
fn values(): Array<String>

All values in order.

returns — value array copy, aligned with `keys()`. Empty values from a key written without `=` are kept as `""`.

import { URLSearchParams } from "@std/web";

print(new URLSearchParams("a=1&flag&b=2").values().join("|"));
Run in Playground
class UrlParts

Parsed URL parts holder used by the parser and resolver. Internal scaffolding; use URL instead. Every field starts as an empty string, and parseAbsolute() fills in whichever parts the text carries. The host and port fields are separate here, while URL also offers a combined host() accessor.

init()

fields

  • fragment: String
  • host: String
  • path: String
  • port: String
  • query: String
  • scheme: String
enum HttpStatus
  • Accepted
  • BadGateway
  • BadRequest
  • Conflict
  • Created
  • Forbidden
  • Found
  • GatewayTimeout
  • InternalServerError
  • MethodNotAllowed
  • MovedPermanently
  • NoContent
  • NotFound
  • NotModified
  • Ok
  • ServiceUnavailable
  • Unauthorized

Functions

fn appendByte(out: String, b: Int): String

Append one %XX escape to a string under construction.

out — text built so far.

b — byte value 0..255.

returns — out with the escape for b added, using uppercase hex.

import { appendByte } from "@std/web";

print(appendByte("%", 65));
print(appendByte(appendByte("", 0), 255));
Run in Playground
fn appendChunks(buf: Array<Int>, pos: Int, stop: Int, out: Array<Int>)

Copy the payload of every chunk in a range, skipping size lines, the CRLF after each, and the trailing CRLF of each chunk. Chunk extensions are skipped with the size line.

buf — bytes holding the chunked body.

pos — index where the first size line starts.

stop — exclusive end, as returned by `chunkTerminalEnd()`.

out — array the payload bytes are appended to in place.

import { strBytes, findHeadersEnd, chunkTerminalEnd, appendChunks, bytesToString } from "@std/web";

let eol = "\u{D}\u{A}";
let raw = strBytes("HTTP/1.1 200 OK" + eol + eol + "4" + eol + "Wiki" + eol + "0" + eol + eol);
let h = findHeadersEnd(raw);
let body: Array<Int> = [];
appendChunks(raw, h, chunkTerminalEnd(raw, h), body);
print(bytesToString(body));
Run in Playground
fn asciiBlock(data: Array<Int>, start: Int, to: Int): String

Decode an all-ASCII byte range, 64 bytes at a time so the concatenation stays linear instead of quadratic.

data — byte values; every byte in the range must be below 128.

start — first index, inclusive.

to — last index, exclusive.

returns — text for the range.

import { strBytes, asciiBlock } from "@std/web";

let b = strBytes("abcdef");
print(asciiBlock(b, 1, 4));
Run in Playground
fn asciiChunk(data: Array<Int>, start: Int, to: Int): String

Decode a short all-ASCII byte range one character at a time. This is the inner step of asciiBlock(); use asciiBlock() for anything larger than a chunk.

data — byte values; every byte in the range must be below 128.

start — first index, inclusive.

to — last index, exclusive.

returns — text for the range.

import { strBytes, asciiChunk } from "@std/web";

let b = strBytes("abcdef");
print(asciiChunk(b, 0, 3));
Run in Playground
fn bodyDone(buf: Array<Int>, hend: Int, mode: Int, want: Int): Bool

True when the framing says the body is complete, which is how the read loop knows to stop.

buf — bytes received so far.

hend — index just past the header block.

mode — framing: 0 close-delimited, 1 Content-Length, 2 chunked.

want — byte count for mode 1; ignored otherwise.

returns — true when the body is complete. Mode 0 never reports done: a close-delimited body ends when the stream does, so the read loop runs until the socket closes.

import { strBytes, findHeadersEnd, bodyDone } from "@std/web";

let eol = "\u{D}\u{A}";
let raw = strBytes("HTTP/1.1 200 OK" + eol + eol + "hello");
let h = findHeadersEnd(raw);
print(bodyDone(raw, h, 1, 5), bodyDone(raw, h, 1, 9), bodyDone(raw, h, 0, 0));
Run in Playground
fn bytesToString(data: Array<Int>): String

Decode raw bytes as UTF-8 text. Blocks keep string building linear; a trailing split multibyte sequence carries into the next block. A byte that cannot start or finish a valid sequence is not replaced with a substitution character: the percent form is left in the output. There is no charset sniffing, so the input is always read as UTF-8.

data — byte values 0..255.

returns — decoded text, "" for empty input.

import { strBytes, bytesToString } from "@std/web";

print(bytesToString(strBytes("ok")));
print(bytesToString(strBytes("a\u{E9}b")));
print(bytesToString([]));
Run in Playground
fn chunkLineEnd(buf: Array<Int>, pos: Int): Int

Find the CRLF ending a chunk-size line.

buf — bytes holding the chunked body.

pos — index where the size line starts.

returns — index of the CR, or -1 when no CRLF has arrived yet.

import { strBytes, chunkLineEnd } from "@std/web";

let eol = "\u{D}\u{A}";
let raw = strBytes("4" + eol + "Wiki");
print(chunkLineEnd(raw, 0));
Run in Playground
fn chunkLineValue(buf: Array<Int>, pos: Int, end: Int): Int

Parse a chunk-size line as a hex byte count. Anything after a ; (chunk extensions) is ignored, and either hex case works.

buf — bytes holding the chunked body.

pos — index where the size line starts.

end — index of the CR ending the line.

returns — chunk size in bytes, or -1 when the line holds no hex digit or a non-hex character. A size of 0 is valid and means last chunk.

import { strBytes, chunkLineEnd, chunkLineValue } from "@std/web";

let eol = "\u{D}\u{A}";
let raw = strBytes("1A" + eol + "payload" + eol);
print(chunkLineValue(raw, 0, chunkLineEnd(raw, 0)));
Run in Playground
fn chunkTerminalEnd(buf: Array<Int>, pos: Int): Int

Walk a chunked body to its terminating zero-size chunk, recursing chunk by chunk.

buf — bytes received so far, starting at the first size line.

pos — index where the next size line starts.

returns — index where the last chunk begins, -1 when more bytes are needed, or -2 when a size line is malformed.

import { strBytes, findHeadersEnd, chunkTerminalEnd } from "@std/web";

let eol = "\u{D}\u{A}";
let raw = strBytes("HTTP/1.1 200 OK" + eol + eol + "4" + eol + "Wiki" + eol + "0" + eol + eol);
let h = findHeadersEnd(raw);
print(chunkTerminalEnd(raw, h));
print(chunkTerminalEnd(strBytes("zz"), 0));
Run in Playground
fn completePrefix(data: Array<Int>, start: Int, to: Int): Int

Trim a block boundary back so it never lands inside a multi-byte UTF-8 sequence. If the last bytes before to are an unfinished sequence, the boundary moves back to that sequence's lead byte, which bytesToString() then picks up on its next pass.

data — byte values.

start — first index of the block, inclusive.

to — proposed last index, exclusive.

returns — a boundary at or before `to` that starts no partial sequence.

import { strBytes, completePrefix } from "@std/web";

print(completePrefix(strBytes("abc"), 0, 3));
print(completePrefix(strBytes("a\u{E9}"), 0, 2));
Run in Playground
fn decodeBlock(data: Array<Int>, start: Int, to: Int): String

Decode a byte range that holds at least one non-ASCII byte, by percent-encoding it and handing it to decodeComponent(). The range must already be aligned to a sequence boundary; completePrefix() arranges that.

data — byte values.

start — first index, inclusive.

to — last index, exclusive.

returns — UTF-8 decoded text for the range.

import { strBytes, decodeBlock } from "@std/web";

let b = strBytes("a\u{C3}\u{A9}b");
print(decodeBlock(b, 0, b.length));
Run in Playground
fn decodeComponent(s: String, plus: Bool): String

Percent-decode a string, assembling UTF-8 sequences into scalars. A multi-byte escape becomes one scalar only when the continuation bytes are all present and the result is a valid scalar; overlong forms, surrogates, and out-of-range values are rejected.

s — input string.

plus — when true, `+` decodes to space (query strings).

returns — decoded string; malformed `%` sequences pass through literally, so decoding never fails and never throws.

import { decodeComponent, encodeComponent } from "@std/web";

print(decodeComponent("a+b%20c", true));
print(encodeComponent("a b", true));
print(decodeComponent("%E2%9C%93", false));
print(decodeComponent("a+b", false));
Run in Playground
fn defaultPort(scheme: String): String

Well-known default port per scheme, "" when none. Serialization uses this to drop a port that adds nothing: https://a.com:443/ and https://a.com/ produce the same href, and port() still reports 443 because that field keeps what the text said.

scheme — lowercased scheme, without the colon.

returns — port digits or "".

import { defaultPort } from "@std/web";

print(defaultPort("https"), defaultPort("http"), defaultPort("ftp"));
Run in Playground
fn encodeComponent(s: String, form: Bool): String

Percent-encode a string as UTF-8 with uppercase hex. Every scalar above 127 becomes its UTF-8 bytes, each written as %XX. URLSearchParams.toString() uses this with form set, which is why a space turns into + there.

s — input string.

form — when true, space encodes as `+` (form encoding); otherwise space encodes as `%20` (URL encoding).

returns — encoded string; unreserved scalars pass through.

import { encodeComponent } from "@std/web";

print(encodeComponent("a b&c=d", false));
print(encodeComponent("a b&c=d", true));
print(encodeComponent("safe-._~", true));
Run in Playground
fn fetch(url: String, options: RequestOptions): Promise<Response>

Minimal HTTP/1.1 client over @std/net.TcpStream: http:// only, Connection: close requests, Content-Length / close-delimited / chunked bodies. Rejects on DNS, connect, framing, and truncation failures.

The request it builds is a plain HTTP/1.1 request line plus Host, Connection: close, User-Agent: Rasmalai/1.0, and a wildcard Accept, then the headers from RequestOptions, then Content-Length when a body was set, then a blank line and the body. Cookies, redirects, compression, keep-alive, and HTTP/2 are all out of scope: a redirect comes back as a 3xx response for you to handle, and every call opens a fresh connection. Body and response text are ASCII plus whatever UTF-8 the bytes hold, decoded by bytesToString(). There is no timeout, so a server that accepts and never answers leaves the call waiting.

A 4xx or 5xx resolves normally; only transport and framing problems reject.

url — `http://host[:port]/path` URL.

options — `RequestOptions` instance, or null for GET.

returns — response promise.

fn fetchInner(url: String, method: String, extra: Headers, body: String): Promise<Response>

Build the request bytes and dispatch to TCP or TLS. This is the part of fetch() that reads the options, so call it directly only if you already have a method, header table, and body in hand.

url — absolute `http://` or `https://` URL.

method — HTTP method, sent as written.

extra — request headers, sent after the built-in ones.

body — request body text; "" sends no body and no `Content-Length`.

returns — response promise.

fn fetchOverTcp(host: String, port: Int, req: String): Promise<Response>

Send a prepared request over plain TCP and parse the reply. Reads the status line, folds every header name to lowercase, then picks the body framing: chunked when Transfer-Encoding says chunked, otherwise Content-Length when the header is there, otherwise read to close. Transfer-Encoding: chunked wins over Content-Length, matching HTTP/1.1. Only the status line's first two fields are required; the reason phrase is kept as sent and may be empty.

host — DNS name or IP literal, already lowercased.

port — TCP port.

req — full request text, headers and body included.

returns — response promise.

fn fetchOverTls(host: String, port: Int, req: String): Promise<Response>

Send a prepared request over TLS and parse the reply. Opens a TCP connection, wraps it in a TlsStream using host as the SNI and certificate domain, then does the same status line, header, and body framing work as fetchOverTcp().

host — DNS name or IP literal; also the TLS server name.

port — TCP port, normally 443.

req — full request text, headers and body included.

returns — response promise.

fn findHeadersEnd(data: Array<Int>): Int

Find the end of an HTTP header block, which is the blank line after the last header. Only CRLF counts, so this waits for more bytes when a server sends bare LF line endings.

data — bytes received so far.

returns — index just past the CRLFCRLF, or -1 when the block has not arrived in full.

import { strBytes, findHeadersEnd } from "@std/web";

let eol = "\u{D}\u{A}";
let raw = strBytes("HTTP/1.1 200 OK" + eol + eol);
print(findHeadersEnd(raw));
print(findHeadersEnd(strBytes("no blank line yet")));
Run in Playground
fn hexDigit(v: Int): String

One uppercase hex digit for a nibble, the encoder side of hexPair().

v — value 0..15.

returns — single character "0" through "9", then "A" through "F".

import { hexDigit } from "@std/web";

print(hexDigit(0), hexDigit(9), hexDigit(15));
Run in Playground
fn hexPair(hi: String, lo: String): Int

Decode two hex characters into a byte value, one %XX escape without the percent sign.

hi — high nibble character.

lo — low nibble character.

returns — 0..255, or -1 when either character is not hex.

import { hexPair } from "@std/web";

print(hexPair("4", "1"), hexPair("f", "F"), hexPair("z", "0"));
Run in Playground
fn hexVal(code: Int): Int

Value of one hex digit. Both cases count, so this reads a percent-escape written by any producer.

code — scalar code from `charCodeAt()`.

returns — 0..15, or -1 when the scalar is not a hex digit.

import { hexVal } from "@std/web";

print(hexVal("f".charCodeAt(0)), hexVal("A".charCodeAt(0)), hexVal("z".charCodeAt(0)));
Run in Playground
fn isAllDigits(s: String): Bool

True for every-char-digits strings (empty counts as false). Used to tell a port from a host when splitting an authority, where an empty or non-numeric tail means there was no port at all.

s — input string.

returns — true when s is non-empty and all ASCII digits.

import { isAllDigits } from "@std/web";

print(isAllDigits("8080"), isAllDigits(""), isAllDigits("80a"));
Run in Playground
fn isAsciiRange(data: Array<Int>, start: Int, to: Int): Bool

True when every byte in a range is below 128.

data — byte values.

start — first index, inclusive.

to — last index, exclusive.

returns — true for an all-ASCII range; an empty range counts as ASCII.

import { strBytes, isAsciiRange } from "@std/web";

let b = strBytes("ok");
print(isAsciiRange(b, 0, b.length), isAsciiRange(strBytes("a\u{E9}b"), 0, 1));
Run in Playground
fn isClientError(code: Int): Bool

True for 4xx codes. A 4xx means the request needs to change before it can succeed.

code — numeric status code.

returns — true when 400 <= code < 500.

import { isClientError } from "@std/web";

print(isClientError(404), isClientError(500), isClientError(399));
Run in Playground
fn isDigitCode(code: Int): Bool

True for the scalar codes of ASCII 0-9.

code — scalar code from `charCodeAt()`.

returns — true when 48 <= code <= 57.

import { isDigitCode } from "@std/web";

print(isDigitCode("7".charCodeAt(0)), isDigitCode("a".charCodeAt(0)));
Run in Playground
fn isSchemeCode(code: Int, first: Bool): Bool

True for a scalar allowed in a URL scheme, per RFC 3986.

code — scalar code from `charCodeAt()`.

first — true for the first character of the scheme, which may not be a digit, `+`, `-`, or `.`.

returns — true when the scalar fits at that position.

import { isSchemeCode } from "@std/web";

print(isSchemeCode("h".charCodeAt(0), true), isSchemeCode("1".charCodeAt(0), true));
print(isSchemeCode("1".charCodeAt(0), false), isSchemeCode("+".charCodeAt(0), true));
Run in Playground
fn isServerError(code: Int): Bool

True for 5xx codes. A 5xx means the request was fine and the server failed; retrying can help.

code — numeric status code.

returns — true when 500 <= code < 600.

import { isServerError } from "@std/web";

print(isServerError(503), isServerError(404), isServerError(600));
Run in Playground
fn isSuccess(code: Int): Bool

True for 2xx codes. Works on any number, so it also answers for codes HttpStatus has no variant for, such as 207.

code — numeric status code.

returns — true when 200 <= code < 300.

import { isSuccess } from "@std/web";

print(isSuccess(204), isSuccess(301), isSuccess(199));
Run in Playground
fn isUnreserved(code: Int): Bool

True for the scalars RFC 3986 calls unreserved: letters, digits, and - . _ ~. These pass through percent-encoding untouched because no layer has to escape them.

code — scalar code from `charCodeAt()`.

returns — true for unreserved scalars, false for everything else.

import { isUnreserved } from "@std/web";

print(isUnreserved("A".charCodeAt(0)), isUnreserved("~".charCodeAt(0)));
print(isUnreserved("/".charCodeAt(0)), isUnreserved(" ".charCodeAt(0)));
Run in Playground
fn lessThan(a: String, b: String): Bool

Lexicographic less-than for strings, scalar by scalar. Compares scalar values, not UTF-8 bytes, and a prefix sorts before the longer string that starts with it. Used to order parameter names.

a — left string.

b — right string.

returns — true when a sorts before b.

import { lessThan } from "@std/web";

print(lessThan("abc", "abd"), lessThan("ab", "abc"), lessThan("abc", "abc"));
Run in Playground
fn lowerAscii(s: String): String

ASCII-only lowercase fold for header names and schemes.

s — input string.

returns — s with A-Z mapped to a-z; other scalars untouched. Bytes above 127 stay as they are, so this never changes text outside ASCII.

import { lowerAscii } from "@std/web";

print(lowerAscii("Content-Type"));
print(lowerAscii("ABC123-_."));
Run in Playground
fn mergePaths(basePath: String, refPath: String): String

Merge a relative reference path onto a base path: everything up to and including the base's last / is kept, and the reference is appended. A base with no / at all cannot be merged, so the reference becomes the whole path under the root.

basePath — path of the base URL.

refPath — path of the relative reference, without a leading `/`.

returns — merged path.

import { mergePaths } from "@std/web";

print(mergePaths("/a/b/", "c"));
print(mergePaths("", "c"), mergePaths("/a", "c"));
Run in Playground
fn parseAbsolute(s: String, parts: UrlParts): Bool

Parse an absolute URL into parts. Surrounding whitespace is trimmed, the scheme is required and lowercased, and //authority is optional, so mailto:[email protected] parses with no host. A hosted URL with an empty path gets /. Dot segments are left alone here; only relative resolution removes them.

s — candidate URL text.

parts — output holder; every field is overwritten.

returns — true when s carries a valid scheme; false otherwise.

import { parseAbsolute, UrlParts } from "@std/web";

let p = new UrlParts();
print(parseAbsolute("https://a.com/x?y=1#f", p), p.scheme, p.host, p.path);
print(parseAbsolute("/no/scheme", p));
Run in Playground
fn parseAuthority(auth: String, parts: UrlParts)

Split an authority into host and port. The host is lowercased; the port is kept as digits, without a leading :. A colon with a non-numeric tail is not a port, so the whole string stays the host: that keeps IPv6 literals and host: from being mangled.

auth — authority text, without the leading `//`.

parts — output holder; `host` and `port` are overwritten.

import { parseAuthority, UrlParts } from "@std/web";

let p = new UrlParts();
parseAuthority("Example.COM:8080", p);
print(p.host, p.port);
Run in Playground
fn parseDec(s: String): Int

Parse a non-negative decimal integer.

s — digit string, "" for none.

returns — the value, or -1 when s is empty or holds a non-digit. The -1 marks failure; valid input never parses to it.

import { parseDec } from "@std/web";

print(parseDec("404"), parseDec(""), parseDec("4a"));
Run in Playground
fn readBodyInto(s: TcpStream, buf: Array<Int>, hend: Int, mode: Int, want: Int): Promise<Array<Int>>

Read a response body, stopping when bodyDone() says the framing is satisfied or the stream ends. A stream that ends early does not throw here; the caller compares the byte count against the framing and decides whether that is a truncated body.

s — stream to read.

buf — buffer appended in place, already holding the headers.

hend — index just past the header block.

mode — framing: 0 close-delimited, 1 Content-Length, 2 chunked.

want — byte count for mode 1; ignored otherwise.

returns — body bytes, without the chunk headers in mode 2.

fn readBodyIntoTls(s: TlsStream, buf: Array<Int>, hend: Int, mode: Int, want: Int): Promise<Array<Int>>

Read a response body over TLS, stopping when bodyDone() says the framing is satisfied or the stream ends. An early end does not throw here; the caller compares the byte count and raises the truncated body message.

s — stream to read.

buf — buffer appended in place, already holding the headers.

hend — index just past the header block.

mode — framing: 0 close-delimited, 1 Content-Length, 2 chunked.

want — byte count for mode 1; ignored otherwise.

returns — body bytes, without the chunk headers in mode 2.

fn readHeaders(s: TcpStream, buf: Array<Int>): Promise<Int>

Read until the CRLFCRLF that ends the header block shows up in the buffer. Bytes past the header end are left in the buffer, since they are usually the start of the body.

s — stream to read.

buf — buffer appended in place.

returns — index just past the header block, or -1 when the stream ends first.

fn readHeadersTls(s: TlsStream, buf: Array<Int>): Promise<Int>

Read until the CRLFCRLF that ends the header block shows up in the buffer, over TLS. Bytes past the header end stay in the buffer.

s — stream to read.

buf — buffer appended in place.

returns — index just past the header block, or -1 when the stream ends first.

fn readMore(s: TcpStream, buf: Array<Int>): Promise<Int>

Read one chunk from a stream and append it to a growing buffer.

s — stream to read.

buf — buffer appended in place, so the caller keeps the bytes it already had.

returns — count of bytes appended, 0 at end of stream.

fn readMoreTls(s: TlsStream, buf: Array<Int>): Promise<Int>

Read one chunk from a TLS stream and append it to a growing buffer.

s — stream to read.

buf — buffer appended in place.

returns — count of bytes appended, 0 at end of stream.

fn removeDotSegments(path: String): String

Resolve dot segments in a path, preserving empty segments. . drops a segment, .. drops one and its parent, and a trailing .. or . leaves the trailing slash in place. A .. at the root is dropped, since there is nothing above it.

path — slash-separated path.

returns — normalized path.

import { removeDotSegments } from "@std/web";

print(removeDotSegments("/a/b/../c"));
print(removeDotSegments("/a/./b/"));
print(removeDotSegments("/a/../../b"));
Run in Playground
fn sliceBody(buf: Array<Int>, hend: Int, mode: Int, want: Int): Array<Int>

Cut the body out of the receive buffer according to its framing.

buf — bytes received so far.

hend — index just past the header block.

mode — framing: 0 close-delimited, 1 Content-Length, 2 chunked.

want — byte count for mode 1; ignored otherwise.

returns — body bytes: exactly `want` for mode 1, everything to the end for mode 0, and the concatenated chunk payloads for mode 2. A mode 2 body that is not fully framed comes back empty.

import { strBytes, findHeadersEnd, sliceBody, bytesToString } from "@std/web";

let eol = "\u{D}\u{A}";
let raw = strBytes("HTTP/1.1 200 OK" + eol + eol + "hello");
print(bytesToString(sliceBody(raw, findHeadersEnd(raw), 1, 5)));
Run in Playground
fn sliceBytes(buf: Array<Int>, start: Int, to: Int): Array<Int>

Copy a range of a byte array.

buf — source bytes.

start — first index, inclusive.

to — last index, exclusive.

returns — new array holding `buf[start..to]`. Empty when start >= to.

import { strBytes, sliceBytes } from "@std/web";

let b = strBytes("abcdef");
print(sliceBytes(b, 1, 4).length, sliceBytes(b, 4, 2).length);
Run in Playground
fn splitFragQuery(rest: String, parts: UrlParts): String

Peel the fragment and then the query off a URL tail, storing both in the holder. The fragment is cut first because anything after a # belongs to it, including a ?.

rest — text after the scheme and authority.

parts — output holder; `query` and `fragment` are overwritten.

returns — the remaining path text.

import { splitFragQuery, UrlParts } from "@std/web";

let p = new UrlParts();
print(splitFragQuery("/a/b?x=1#f", p), p.query, p.fragment);
Run in Playground
fn statusCode(s: HttpStatus): Int

Numeric code for a status.

s — status to convert.

returns — e.g. 200 for Ok, 404 for NotFound. Every variant maps to a fixed code; there is no "other" case.

import { HttpStatus, statusCode } from "@std/web";

print(statusCode(HttpStatus.NotFound));
print(statusCode(HttpStatus.Ok));
Run in Playground
fn statusFromCode(code: Int): HttpStatus?

Look up a status by numeric code, the reverse of statusCode(). Only the 17 codes named by HttpStatus are known; anything else has no variant, which is why the result is nullable.

code — numeric status code, as parsed off a status line.

returns — the matching variant, or `null` when unknown.

import { HttpStatus, statusCode, statusFromCode } from "@std/web";

print(statusCode(statusFromCode(201) ?? HttpStatus.Ok));
print(statusFromCode(999) == null);
Run in Playground
fn statusReason(s: HttpStatus): String

Reason phrase for a status, the text a server puts after the code in a status line.

s — status to convert.

returns — e.g. "OK" for Ok, "Not Found" for NotFound. The phrase is fixed per variant, not read off the wire.

import { HttpStatus, statusReason } from "@std/web";

print(statusReason(HttpStatus.NoContent));
print(statusReason(HttpStatus.ServiceUnavailable));
Run in Playground
fn strBytes(s: String): Array<Int>

String to scalar codes, one array element per character. A character above 127 becomes its UTF-8 bytes, so the array is the UTF-8 encoding of the string and is what a socket write wants.

s — text to encode.

returns — array of byte values 0..255.

import { strBytes } from "@std/web";

let b = strBytes("Hi");
print(b.length, b[0], b[1]);
Run in Playground