Skip to content

http/request ​

ts
import {
request
} from 'mikro/http/request'

Make HTTP requests against a network endpoint. Returns a Result so network-level failures are handled explicitly instead of thrown.

Functions ​

request(url, options?) ​

ts
function request(url: string, options?: RequestOptions): Promise<Result<Response, RequestError>>
ts
const 
result
= await
request
('https://api.example.com/data')
if (!
result
.
ok
) {
console
.
error
('Request failed:',
result
.
error
)
return } const
response
=
result
.
value
console
.
log
('Status: %d',
response
.
status
)
const
data
= await
response
.
json
()
if (!
data
.
ok
) {
console
.
error
('Body decode failed:',
data
.
error
)
return }
console
.
log
(
data
.
value
)

Result vs response.ok

request returns a Result that indicates whether the network request succeeded at all. The Response inside has its own .ok property that indicates whether the HTTP status was 2xx. You need to check both:

ts
const 
result
= await
request
('https://api.example.com/data')
if (!
result
.
ok
) {
// Network error (no connection, DNS failure, etc.) return } if (!
result
.
value
.
ok
) {
// HTTP error (404, 500, etc.)
console
.
error
('HTTP %d',
result
.
value
.
status
)
return } const
data
= await
result
.
value
.
json
()
if (!
data
.
ok
) {
// Body drain or JSON.parse failed mid-stream return }
console
.
log
(
data
.
value
)

POST request ​

ts
const 
result
= await
request
('https://api.example.com/data', {
method
: 'POST',
headers
: {'content-type': 'application/json'},
body
:
JSON
.
stringify
({
temperature
: 23.5}),
})

body accepts a Uint8Array or a UTF-8 string. There is no json: shortcut: stringify the value yourself and set content-type if your endpoint needs it.

Request with timeout ​

ts
const 
result
= await
request
('https://api.example.com/data', {
timeoutMs
: 5000,
}) if (!
result
.
ok
&&
result
.
error
.
name
=== 'Aborted') {
console
.
error
('Request timed out')
}

timeoutMs is a total wallclock deadline. Compose with AbortSignal when you need external cancellation.

On ESP32, cancelling a request terminates the underlying HTTP task between read chunks and frees TLS buffers immediately.

Types ​

RequestOptions ​

ts
interface RequestOptions {
  method?: string
  headers?: [string, string][] | Record<string, string>
  body?: Uint8Array | string
  timeoutMs?: number
  signal?: AbortSignal
}

Response ​

ts
interface Response {
  readonly status: number
  readonly statusText: string
  readonly url: string
  readonly redirected: boolean
  readonly headers: [string, string][]
  readonly ok: boolean // true if status is 200-299
  readonly body: AsyncIterable<Result<Uint8Array, RequestError>>

  get(name: string): string | undefined
  getAll(name: string): string[]
  text(): Promise<Result<string, RequestError>>
  json(): Promise<Result<unknown, RequestError>>
  bytes(): Promise<Result<Uint8Array, RequestError>>
  close(): Promise<void>
}

get and getAll look up headers case-insensitively. text(), json(), bytes(), and body are single-shot: calling more than one drains the body and a second call throws BodyConsumedError. Each method returns a Result so mid-stream network drops, aborts, or (for json()) JSON parse failures stay inside the Result chain instead of throwing.

Always close responses you don't read

Every response you don't drain with text(), json(), bytes(), or a for await loop needs an explicit await response.close(). Skipping this keeps memory tied up until the device reboots, and eventually new requests will fail with TooManyPending.

Errors ​

RequestError ​

Returned by request and any custom transport built on top of mikro/http/helpers.

VariantFieldsDescription
Hardwaremessage: stringUnderlying hardware/driver failure
Networkmessage: stringRequest failed (DNS, TLS, connection, or transport error)
Timeoutmessage: stringRequest exceeded its deadline
BodyTooLargesize: number, cap: numberRequest body exceeded the transport's limit
InvalidResponsemessage: stringResponse was malformed or couldn't be parsed
Abortedmessage: stringRequest was cancelled via timeout or AbortSignal
TooManyPending—Transport's in-flight request slots are full
InvalidJsonmessage: stringResponse.json() drained the body but JSON.parse failed

BodyConsumedError ​

Thrown by Response.text(), json(), bytes(), or a second iteration of body after the body has already been drained.

Custom transports ​

The default request is backed by a WiFi-driven HTTP client. If you're driving HTTP through something else (for example an LTE modem exposing an AT-command HTTP stack), write a function with the same Request signature using helpers from mikro/http/helpers for the boring parts:

ts
export const 
request
:
Request
= async (
url
,
opts
= {}) => {
const {
body
,
headers
} =
prepareBody
(
opts
)
// Drive your physical transport here. On failure return // err(RequestError.Network(msg)). const
raw
= await driveTheTransport(
url
,
opts
.
method
?? 'GET',
body
,
headers
)
if (!
raw
.ok) return
err
(
RequestError
.
Network
(
raw
.error))
return
ok
(
makeResponse
({
status
:
raw
.value.status,
statusText
: '',
url
,
redirected
: false,
headers
:
raw
.value.headers,
body
:
raw
.value.body, // AsyncIterable<Result<Uint8Array, RequestError>>
}), ) }

Import from mikro/http/helpers instead of mikro/http/request when you don't need the default implementation; that avoids paying for its WiFi/TLS machinery at startup.

Mikro.js is built with AI assistance, code and docs alike. Read the AI disclosure.