The authenticated request helper shared by the microsoft-* packages,
plus Graph-shaped errors, retry classification, OData query
building, @odata.nextLink paging, and JSON batching.
Paths. path is either a Graph path ("/me/drive", sent to
{graph}/v1.0/me/drive for the call's cloud; opts.api-version: "beta" switches version) or a full https:// URL such as an
@odata.nextLink. A full URL gets credentials only on the call's
Graph root or a Microsoft Graph cloud host (see
may-receive-credentials); anything else is an Err, and nothing is
sent or minted.
Retry. microsoft-graph never waits between attempts. A connection
failure on an idempotent method (GET, HEAD, PUT, DELETE, OPTIONS) is
retried once, immediately (opts.retry: false turns that off).
Everything else, including 429 and 503 throttling, comes back as
err(HttpError) with retry-after set from the Retry-After
header. Check it with is-retryable and let Hot back off: give the
handler, schedule or task a retry policy and fail the run (see
https://hot.dev/docs/retries).
401 handling. When a cached token is rejected with 401, it is evicted and the call is retried once with a freshly minted token.
Request ids. Every call sends a fresh client-request-id (or
opts.client-request-id; false sends none) and asks Graph to echo
it. HttpError carries it with Graph's request-id, which is what
Microsoft support asks for.
Values
IDEMPOTENT_METHODS
IDEMPOTENT_METHODS: Vec [
"GET",
"HEAD",
"PUT",
"DELETE",
"OPTIONS"
]
MAX_BATCH_SIZE
MAX_BATCH_SIZE: Int 20
MAX_IMMEDIATE_RETRIES
MAX_IMMEDIATE_RETRIES: Int 1
MAX_PAGES
MAX_PAGES: Int 10000
ODATA_OPTIONS
ODATA_OPTIONS: Map {
select: "$select",
expand: "$expand",
filter: "$filter",
orderby: "$orderby",
order-by: "$orderby",
top: "$top",
skip: "$skip",
search: "$search",
count: "$count",
skiptoken: "$skiptoken"
}
RETRY_STATUSES
RETRY_STATUSES: Vec [
408,
429,
500,
502,
503,
504
]
Functions
api-params
fn (opts: Map): Map
Strip microsoft-graph's own keys (::microsoft::graph::auth/CREDENTIAL_KEYS: credentials, cloud, scopes, retry, headers, ...) from an opts Map, leaving only API parameters.
batch
fn (entries: Vec): Map
fn (entries: Vec, opts: Map): Map
POST /$batch - send up to 20 requests in one call. Each entry is
{id?, method, url, headers?, body?, depends-on?} with url a Graph
path ("/me/drive/root/children"). Returns {responses, by-id, failed, retryable} (see order-responses); the batch itself fails
only when the whole request does. Individual 429s are listed in
retryable: resend just those later. Use batch-chunks for more
than 20.
out ::microsoft::graph::api/batch([
{id: "me", method: "GET", url: "/me?$select=id,displayName"},
{id: "drive", method: "GET", url: "/me/drive?$select=id,quota"}
])
get(out.by-id, "drive").body.quota
batch-body
fn (body: Any): Any
batch-chunks
fn (entries: Vec): Vec
Split entries into groups of at most 20 for batch (pure).
body-excerpt
fn (body: Any): Str
A short (at most 300 characters) printable form of a response body, for error messages.
build-batch
fn (entries: Vec): Map
Build a $batch body {requests: [...]} from entries (pure); an Err for zero or more than 20 entries or duplicate ids.
build-batch-request
fn (entry: Map, index: Int): Map
Normalise one batch entry (pure): {id?, method, url, headers?, body?, depends-on?}. id defaults to the 1-based position; url
is made relative; a Map/Vec body gets Content-Type: application/json; a Bytes body is base64-encoded (Graph's form for
binary bodies); depends-on (Vec of ids) becomes dependsOn.
build-http-request
fn (method: Str, url: Str, headers: Map, body: Any, token: Str?, request-id: Str?): ::hot::http/HttpRequest
Build the outgoing HttpRequest (pure). token (nullable) becomes
the Bearer header; request-id the client-request-id header with
return-client-request-id: true. Map/Vec bodies are JSON-encoded
with a JSON Content-Type; Str/Bytes bodies are sent verbatim (set
Content-Type in headers); "" or null sends no body. Caller
headers win.
client-request-id
fn (opts: Map): Str?
The client-request-id to send (pure apart from the UUID): opts.client-request-id when a Str, none when it is false, else a fresh UUID.
collect-loop
fn (fetch: Fn, limit: Int?, next-link: Str?, acc: Vec, pages: Int): Vec
collect-pages
fn (path: Str, opts: Map): Vec
fn (path: Str, opts: Map, limit: Int?): Vec
Follow @odata.nextLink and collect every item of path, or at most
limit items (null for no limit). opts holds OData parameters and
credentials as for fetch-page. The first failing page's Err is
returned.
users ::microsoft::graph::api/collect-pages("/users", {select: ["id", "mail"], top: 999}, 5000)
collect-with
fn (fetch: Fn, limit: Int?): Vec
Collect items with your own page function: fetch is (next-link: Str?) -> Page (null for the first page). Stops at limit items (null for all).
credential-opts
fn (opts: Map): Map
Keep only microsoft-graph's own keys (credentials, cloud, scopes, retry, headers, ...) from an opts Map, e.g. to pass them on to a follow-up call.
decode-error-body
fn (body: Any): Any
encode-query-key
fn (key: Str): Str
encode-segment
fn (value: Str): Str
URL-encode one path segment (an id, a user principal name, a site id with commas, ...).
fetch-page
fn (path: Str, opts: Map, next-link: Str?): Page
Fetch one page: GET next-link when given, else path with the
OData parameters in opts. The query's ConsistencyLevel header is
sent on every page (Graph does not carry it over).
page ::microsoft::graph::api/fetch-page("/users", {select: "id,displayName", top: 100}, null)
page.items
next ::microsoft::graph::api/fetch-page("/users", {select: "id,displayName", top: 100}, page.next-link)
get-json
fn (path: Str, opts: Map): Any
GET a Graph path with OData query parameters taken from opts (see
odata-params; credential keys are left for auth), sending
ConsistencyLevel: eventual when $search / $count need it.
users ::microsoft::graph::api/get-json("/users", {select: ["id", "displayName"], search: "displayName:alex", count: true})
graph-error-details
fn (body: Any): Map
Extract Graph's error fields from a response body (pure): {code, message, inner-code, request-id, client-request-id}. Handles the
standard {error: {code, message, innerError: {request-id, client-request-id, code?}}} shape (also innererror), and the
OAuth-style {error: "invalid_token", error_description} shape.
Fields are null when absent.
has-code
fn (error: Result, code: Str): Bool
True when error (an HttpError, a Result.Err holding one, or any error payload) carries the Graph error code (or inner code).
header-value
fn (headers: Any, name: Str): Str?
A response header by lower-case name, or null when absent or empty.
innermost-code
fn (inner: Any, found: Str?): Str?
is-connection-failure
fn (message: Str): Bool
True when a transport error message is a connection or read failure (worth trying again), not a size-limit or URL error.
is-full-url
fn (path: Str): Bool
is-retryable
fn (error: Result): Bool
True when a failed call is worth retrying later: an HttpError (or a
Result.Err holding one, or a raw HttpResponse) with status 408,
429, 500, 502, 503 or 504; a TokenError from the token endpoint
with one of those statuses; or a connection failure. False for
successes and for permanent errors (400, 401, 403, 404, 409, 410, ...).
microsoft-graph does not wait and retry for you: Hot has no in-run
sleep. Give the handler, schedule or task a retry policy (see
https://hot.dev/docs/retries) and fail the run when this returns
true; retry-after on the HttpError says how long Graph asked you
to wait.
sync-files
meta {
schedule: "every 15 minutes",
retry: {attempts: 5, delay: 5000, backoff: "exponential", max_delay: 120000, jitter: true}
}
fn (event) {
me ::microsoft::graph::api/json("GET", "/me/drive", {})
cond {
::microsoft::graph::api/is-retryable(me) => { fail(`graph: retrying later: ${err-message(me)}`) }
is-err(me) => { println(`graph: giving up: ${err-message(me)}`) }
=> { me.id }
}
}
is-retryable-status
fn (status: Int): Bool
True for statuses Graph asks clients to retry later: 408, 429 (throttled), 500, 502, 503 (service unavailable, often with Retry-After) and 504.
is-retryable-transport
fn (method: Str, message: Str): Bool
True when a transport failure may be retried immediately: an idempotent method (GET, HEAD, PUT, DELETE, OPTIONS) and a connection failure.
json
fn (method: Str, path: Str): Any
fn (method: Str, path: Str, opts: Map): Any
fn (method: Str, path: Str, headers: Map, body: Any, opts: Map): Any
Like request, but unwraps: the decoded JSON body on 2xx ({} for 204 No Content), or an Err carrying HttpError (credential failures carry ::microsoft::graph::auth/TokenError). Takes the same arities as request.
may-receive-credentials
fn (url: Str, graph: Str): Bool
True when a full URL may receive credentials (pure): https on the
host of graph (the call's resolved Graph root) or of a Microsoft
Graph cloud (::microsoft::graph/CLOUDS). http only on the host of
a graph root that is itself http, for tests and local proxies.
Hosts match exactly, so graph.microsoft.com.evil.example is refused.
::microsoft::graph::api/may-receive-credentials("https://graph.microsoft.us/v1.0/me", "https://graph.microsoft.com") // true
::microsoft::graph::api/may-receive-credentials("https://evil.example/v1.0/me", "https://graph.microsoft.com") // false
needs-eventual-consistency
fn (params: Map): Bool
True when query parameters use $search or $count, which Graph's advanced directory queries only accept with ConsistencyLevel: eventual (pure).
odata-key
fn (key: Str): Str
The query key for an option name: select becomes $select (see ODATA_OPTIONS); keys already starting with $ and any other names pass through.
odata-params
fn (params: Map): Map
Normalise query parameters (pure): microsoft-graph keys are removed,
nulls dropped, OData short names (select, expand, filter,
orderby, top, skip, search, count) become their $ keys,
Vec values are comma-joined (select: ["id", "name"]), Bools become
true/false, and a $search value is double-quoted.
::microsoft::graph::api/odata-params({select: ["id", "displayName"], top: 50, filter: null})
// {"$select": "id,displayName", "$top": "50"}
odata-value
fn (key: Str, value: Any): Str
order-responses
fn (requests: Vec, reply: Map): Map
Parse a $batch reply (pure): {responses: Vec<BatchResponse>} in the order of requests (Graph answers in any order), plus by-id, the ids that failed, and those that are retryable.
parse-batch-response
fn (r: Map): BatchResponse
Read one entry of a $batch reply's responses into a BatchResponse (pure).
plain-payload
fn (value: Result): Any
The payload of a Result.Err (or the value itself), with type tags removed so an HttpError / TokenError reads like a Map.
query-headers
fn (params: Map): Map
The extra headers a query needs (pure): {ConsistencyLevel: "eventual"} when needs-eventual-consistency, else {}. Send them on every page of a paged query.
query-string
fn (params: Map): Str
Encode normalised parameters as a query string (pure). Values are
percent-encoded per RFC 3986 (spaces as %20, never +, which some
Graph services read literally); the $ of OData keys stays literal.
Keys are sorted, so output is stable.
quote-search
fn (value: Str): Str
Wrap a $search value in double quotes unless it already is (Graph requires $search="..." for directory objects and messages). Embedded " are escaped as \\".
refused-url-error
fn (url: Str): Any
relative-url
fn (url: Str): Str
The batch form of a URL (pure): the path after /v1.0 (or /beta) for a full Graph URL, with a leading /; relative paths pass through.
request
fn (method: Str, path: Str): ::hot::http/HttpResponse
fn (method: Str, path: Str, opts: Map): ::hot::http/HttpResponse
fn (method: Str, path: Str, headers: Map, body: Any, opts: Map): ::hot::http/HttpResponse
Make an authenticated request to Microsoft Graph and return the raw
HttpResponse (non-2xx included), or an Err for credential and
transport failures.
method-"GET","POST","PATCH","PUT","DELETE"path- a Graph path ("/me/drive/root/children") or a full URL on the call's Graph root or a Microsoft Graph cloud host; any other host is an Err, with no credentials sent or mintedheaders- extra headers (they win over the defaults andopts.headers)body- a Map/Vec (sent as JSON), Str/Bytes (sent verbatim), or""opts- per-call credentials (see::microsoft::graph::auth),headers,api-version,client-request-id, andretry(falsedisables the one immediate retry of a failed connection)
response ::microsoft::graph::api/request("GET", "/me", {})
// As one tenant user, without touching context
response2 ::microsoft::graph::api/request("GET", "/me/drive", {}, "", {token: "eyJ0eXAi..."})
resolve
fn (): Map
fn (opts: Map): Map
The package's one credential and settings resolver: per-call opts
first, then context, then defaults (see ::microsoft::graph::auth).
Returns resolved credentials {kind, cloud, ...} or a TokenError:
a credential key in opts that is blank or null, and graph-url /
login-url in opts without a credential, are errors, never a
fallback to context. Credentials from opts are marked as secrets
(masked in call records) and never written to context.
resolve-url
fn (path: Str, graph: Str, opts: Map): Str
The absolute URL for path (pure): full http(s):// URLs pass
through; a Graph path is joined to {graph}/{version} (v1.0
unless opts.api-version is set). A missing leading / is added.
retry-after-seconds
fn (headers: Map?): Int?
The Retry-After header of a response's headers as whole seconds (Int), or null when absent or given as an HTTP date.
retry-policy
fn (opts: Map): Map
The immediate-retry policy for a call (pure): {max-retries: 1}, or
{max-retries: 0} when opts.retry is false or 0. Values are
clamped to 0..1: microsoft-graph never waits between attempts, so it
never retries more than once.
send-with-retry
fn (req: ::hot::http/HttpRequest, policy: Map): ::hot::http/HttpResponse
fn (req: ::hot::http/HttpRequest, policy: Map, attempt: Int): ::hot::http/HttpResponse
Send an HttpRequest; on a connection failure of an idempotent method, send it once more straight away (per policy). Never waits. Returns the final response or transport Err.
to-page
fn (response: Map): Page
Read a decoded collection response into a Page (pure).
unwrap
fn (response: Result): Any
The decoded body of a 2xx response ({} for an empty 204), the Err of a failed request, or err(HttpError) for a non-2xx response.
url-parts
fn (url: Str): Map?
The lower-cased {scheme, host} of an http(s) URL (pure), or null
unless the authority is plainly a host and optional port. Userinfo,
backslashes, extra slashes and control characters all give null: URL
parsers disagree on where the host is in those, and a backslash ends
the authority in the one that opens the connection, so
https://evil.example\@graph.microsoft.com/ connects to evil.example.
with-query
fn (url: Str, params: Map): Str
Append query parameters to a URL or path (see odata-params and
query-string), dropping nulls. Works whether or not the URL already
has a query.
::microsoft::graph::api/with-query("/users", {select: "id,mail", filter: "startswith(displayName,'A')", top: 10})
// "/users?$filter=startswith%28displayName%2C%27A%27%29&$select=id%2Cmail&$top=10"
Types
BatchResponse
BatchResponse type {
id: Str,
status: Int,
headers: Map,
body: Any,
ok: Bool,
error: HttpError?,
retry-after: Int?
}
One response inside a $batch reply.
id,status,headers,body(decoded JSON, or the raw value)ok- true for 2xxerror- anHttpErrorfor non-2xx, else nullretry-after- from the individual response'sRetry-After
HttpError
HttpError type {
status: Int,
headers: Map?,
body: Any,
message: Str?,
code: Str?,
inner-code: Str?,
error-message: Str?,
request-id: Str?,
client-request-id: Str?,
retry-after: Int?
}
A non-2xx response from Microsoft Graph.
Fields
status- HTTP status codeheaders,body- the raw response (body is decoded JSON when JSON)message- readable summary, e.g."microsoft-graph: HTTP 404 itemNotFound: The resource could not be found."code- Graph'serror.code(e.g."itemNotFound","accessDenied","activityLimitReached","resyncRequired")inner-code- the innermostinnerError.code, when presenterror-message- Graph'serror.messagerequest-id,client-request-id- frominnerErroror the response headers, for Microsoft supportretry-after- seconds from a numericRetry-Afterheader, else null
Page
Page type {
items: Vec,
next-link: Str?,
delta-link: Str?,
count: Int?,
response: Map
}
One page of a collection: items (value), next-link
(@odata.nextLink, null on the last page), delta-link
(@odata.deltaLink, set on the last page of a delta query), count
(@odata.count when requested), and the decoded response.