The authenticated request helper shared by the google-* packages, plus Google-shaped errors, retry classification, and nextPageToken pagination.

Retry. google-core 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, 5xx and rate-limit 403s, comes back as err(HttpError) with retry-after set from the response's 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 and is-retryable).

401 handling. When a cached token is rejected with 401, it is evicted and the call is retried once with a freshly minted token.

Values

IDEMPOTENT_METHODS

IDEMPOTENT_METHODS: Vec [
  "GET",
  "HEAD",
  "PUT",
  "DELETE",
  "OPTIONS"
]

MAX_IMMEDIATE_RETRIES

MAX_IMMEDIATE_RETRIES: Int 1

MAX_PAGES

MAX_PAGES: Int 10000

RETRY_REASONS

RETRY_REASONS: Vec [
  "rateLimitExceeded",
  "userRateLimitExceeded"
]

RETRY_STATUSES

RETRY_STATUSES: Vec [
  408,
  429,
  500,
  502,
  503,
  504
]

Functions

api-params

fn (opts: Map): Map

Strip google-core's own keys (::google::auth/CREDENTIAL_KEYS: credentials, scopes, retry) from an opts Map, leaving only API parameters.

body-excerpt

fn (body: Any): Str

A short (at most 300 characters) printable form of a response body, for error messages.

build-http-request

fn (method: Str, url: Str, headers: Map, body: Any, token: Str?): ::hot::http/HttpRequest

Build the outgoing HttpRequest. token (nullable) becomes the Bearer header. 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.

collect-loop

fn (fetch: Fn, items-key: Str, limit: Int?, page-token: Str?, acc: Vec, pages: Int): Vec

collect-pages

fn (fetch: Fn, items-key: Str): Vec
fn (fetch: Fn, items-key: Str, limit: Int?): Vec

Follow nextPageToken and collect every item, or at most limit items (null for no limit). fetch and items-key are as for fetch-page. The first failing page's Err is returned.

files ::google::api/collect-pages((t) {
  ::google::drive::files/list-files({q: "trashed = false", pageToken: t, pageSize: 1000})
}, "files", 5000)

credential-opts

fn (opts: Map): Map

Keep only google-core's own keys (credentials, scopes, retry) from an opts Map, e.g. to pass them on to a follow-up call.

decode-error-body

fn (body: Any): Any

fetch-page

fn (fetch: Fn, items-key: Str, page-token: Str?): Page

Fetch one page. fetch is (page-token: Str?) -> Map (null for the first page) returning the decoded list response; items-key names its list field ("files", "drives", "permissions", ...).

page ::google::api/fetch-page((t) { ::google::drive::files/list-files({pageToken: t}) }, "files", null)
page.items
page.next-page-token

google-error-details

fn (body: Any): Map

Extract Google's error fields from a response body: {message, status, reasons}. Handles the standard {error: {code, message, status, errors: [{reason, ...}]}} shape and the OAuth-style {error: "invalid_grant", error_description} shape. Fields are null (and reasons empty) when absent.

has-reason

fn (error: Result, reason: Str): Bool

True when error (an HttpError, a Result.Err holding one, or any error payload) carries the given Google reason.

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-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, or a 403 rate-limit reason; 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 permission, 404, ...).

google-core does not wait and retry for you: Hot has no in-run sleep. Let Hot reschedule the work instead. 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 Google asked you to wait.

sync-inbox
meta {
  schedule: "every 15 minutes",
  retry: {attempts: 5, delay: 2000, backoff: "exponential", max_delay: 60000, jitter: true}
}
fn (event) {
  page ::google::api/json("gmail.readonly", "GET", "https://gmail.googleapis.com/gmail/v1/users/me/messages", {}, "")
  cond {
    ::google::api/is-retryable(page) => { fail(`google: retrying later: ${err-message(page)}`) }
    is-err(page) => { println(`google: giving up: ${err-message(page)}`) }
    => { length(or(page.messages, [])) }
  }
}

A connection failure on a non-idempotent call (sending mail, creating an event) may still have reached Google; retrying it can duplicate the write.

is-retryable-status

fn (status: Int, body: Any): Bool

True for statuses Google asks clients to retry later: 408, 429, 5xx (500/502/503/504), and 403 with reason rateLimitExceeded / userRateLimitExceeded.

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 (scope: Str | Vec, method: Str, url: Str): Any
fn (scope: Str | Vec, method: Str, url: Str, additional-headers: Map, body: Any): Any
fn (scope: Str | Vec, method: Str, url: Str, additional-headers: Map, body: Any, token: Str): Any
fn (scope: Str | Vec, method: Str, url: Str, additional-headers: Map, body: Any, opts: Map): Any

Like request, but unwraps: the decoded JSON body on 2xx, or an Err carrying HttpError (credential failures carry ::google::auth/TokenError). The shape used by all google-* endpoint functions. Takes the same arities as request.

parse-int

fn (value: Any): Int?

Parse an Int from an Int or a decimal-digit Str (surrounding whitespace allowed); null for anything else.

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.

request

fn (scope: Str | Vec, method: Str, url: Str): ::hot::http/HttpResponse
fn (scope: Str | Vec, method: Str, url: Str, additional-headers: Map, body: Any): ::hot::http/HttpResponse
fn (scope: Str | Vec, method: Str, url: Str, additional-headers: Map, body: Any, token: Str): ::hot::http/HttpResponse
fn (scope: Str | Vec, method: Str, url: Str, additional-headers: Map, body: Any, opts: Map): ::hot::http/HttpResponse

Make an authenticated request to a Google API and return the raw HttpResponse (non-2xx included), or an Err for credential and transport failures.

  • scope - the scope(s) the call needs (Str or Vec, short names ok); overridden by opts.scopes / google.scopes
  • additional-headers - extra headers (they win over the defaults)
  • body - a Map/Vec (sent as JSON), Str/Bytes (sent verbatim), or ""
  • opts (6th argument) - a Map of per-call credentials (token, refresh-token + client-id + client-secret, service-account + subject, api-key), scopes, and retry (false disables the one immediate retry of a failed connection). A Str 6th argument is an explicit access token (kept for compatibility). Per-call credentials are masked in call records, and one that is present but blank or null is an Err naming the key.

Example

response ::google::api/request("drive.readonly", "GET",
  "https://www.googleapis.com/drive/v3/about?fields=user", {}, "")

// As a specific tenant user, without touching context
response2 ::google::api/request("drive.readonly", "GET",
  "https://www.googleapis.com/drive/v3/about?fields=user", {}, "",
  {token: "ya29.tenant-access-token"})

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. An Int or {max-retries} Map is accepted and clamped to 0..1: google-core 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.

unwrap

fn (response: Result): Any

The decoded body of a 2xx response, the Err of a failed request, or err(HttpError) for a non-2xx response.

with-query

fn (url: Str, params: Map): Str

Append query parameters to a URL, dropping null values; Bools encode as true/false. A Vec value repeats the key once per element ({labelIds: ["INBOX", "UNREAD"]} gives labelIds=INBOX&labelIds=UNREAD), the way Google's repeated parameters expect. Works whether or not the URL already has a query.

Types

HttpError

HttpError type {
    status: Int,
    headers: Map?,
    body: Any,
    message: Str?,
    reason: Str?,
    reasons: Vec?,
    error-status: Str?,
    error-message: Str?,
    retry-after: Int?
}

A non-2xx response from a Google API: HttpError(response) builds it from the HttpResponse of request.

Fields

  • status - HTTP status code
  • headers, body - the raw response (body is decoded JSON when JSON)
  • message - readable summary, e.g. "google: HTTP 404 NOT_FOUND (notFound): File not found: 1AbC."
  • error-message - Google's error.message
  • error-status - Google's error.status (e.g. "PERMISSION_DENIED")
  • reason - the first error.errors[].reason (e.g. "notFound", "insufficientFilePermissions", "rateLimitExceeded")
  • reasons - every error.errors[].reason
  • retry-after - seconds from a numeric Retry-After header, else null

Page

Page type {
    items: Vec,
    next-page-token: Str?,
    response: Map
}

One page of a list call: items (the list under the endpoint's items key), next-page-token (null on the last page), and the full decoded response.