Skip to Content

HTTP Requests

Glubean’s HTTP client is a thin wrapper around ky  — a popular, well-tested HTTP client for Node.js. All ky APIs are available (retry, timeout, hooks, searchParams, json, etc.). Glubean adds automatic tracing, metrics, and schema validation on top.

In real projects, prefer creating shared clients with configure() and importing those clients into tests, contracts, and workflows. ctx.http still exists as the raw per-test client, and it is useful for scratch probes or when a custom client factory needs access to the runtime HTTP layer.

Three differences from vanilla ky:

  • Leading slashes work with prefixUrlapi.get("/users") just works. Ky normally rejects this ; Glubean strips the leading / automatically.
  • Non-2xx responses don’t throw — ky defaults to throwHttpErrors: true, but in a verification context you need to assert on 4xx/5xx responses, not catch exceptions. Glubean sets throwHttpErrors: false so you always get a response object back. Use ctx.expect(res).toHaveStatus(404) to verify error status codes.
  • Empty searchParams don’t pollute URLs — passing searchParams: {} in ky appends a bare ? to the URL. Glubean removes empty searchParams automatically so your traces stay clean.

Every request automatically produces traces and metrics — no extra setup needed.

Basic usage

import { configure, test } from "@glubean/sdk"; const { http: api } = configure({ http: { prefixUrl: "{{BASE_URL}}", headers: { Authorization: "Bearer {{API_TOKEN}}", }, }, }); export const getUser = test("get-user", async (ctx) => { const users = await api.get("users"); ctx.expect(users).toHaveStatus(200); const created = await api.post("users", { json: { name: "Alice" }, }); ctx.expect(created).toHaveStatus(201); ctx.log("User API checks completed", { listStatus: users.status, createStatus: created.status, }); });

Raw per-test client

Use ctx.http directly only for small probes, scratch-style checks, or when the base URL is intentionally computed inside one test:

export const userFlow = test("user-flow", async (ctx) => { const api = ctx.http.extend({ prefixUrl: ctx.vars.require("BASE_URL"), headers: { Authorization: `Bearer ${ctx.secrets.require("TOKEN")}`, }, }); const user = await api.get("users/1"); ctx.expect(user).toHaveStatus(200); const settings = await api.get("users/1/settings"); ctx.expect(settings).toHaveStatus(200); });

If several files need the same client, move the configure() call into a shared configure.ts file instead of repeating .extend() in each test.

Endpoint attribution with .track()

Contracts know their endpoint shape, so contract.http.with() cases already project to stable endpoint keys such as GET /users/:id.

Raw HTTP calls do not always know that shape. If you call dynamic URLs from a raw test(), use .track() to pin the request to the canonical endpoint the team should review in reports, coverage, and Cloud:

export const getUser = test("get-user", async (ctx) => { const userId = ctx.vars.require("USER_ID"); const res = await api.get(`users/${userId}`).track("GET /users/:id"); ctx.expect(res).toHaveStatus(200); });

Without .track(), evidence may fragment into literal paths such as GET /users/1 and GET /users/2. With .track(), the run evidence, projection, and agent context all point at one authored endpoint.

Request options

OptionTypeDescription
jsonobjectJSON body (auto-serialized)
bodyFormData | stringRaw body
searchParamsRecord<string, string>Query string
headersRecord<string, string>Request headers
timeoutnumber | falseTimeout in ms (default 10000, false to disable)
retrynumber | objectRetry count or { limit, statusCodes, methods }
throwHttpErrorsbooleanThrow on 4xx/5xx (default false)
schemaHttpSchemaOptionsPer-call Zod validation for request/response body, query, and headers. See Inline schema validation below.

Inline schema validation

Pass schema on any call to validate request and response payloads against Zod (or any safeParse / parse-compatible) schemas. Violations are recorded as assertion failures without disrupting the rest of the test.

import { z } from "zod"; const BodySchema = z.object({ name: z.string(), email: z.string().email() }); const ResponseSchema = z.object({ id: z.string(), createdAt: z.string() }); await api.post("users", { json: { name: "Alice", email: "alice@example.com" }, headers: { "X-Tenant-Id": "t1" }, schema: { request: BodySchema, response: ResponseSchema, query: z.object({ version: z.string() }), requestHeaders: z.object({ "X-Tenant-Id": z.string() }), responseHeaders: z.object({ "content-type": z.string() }), }, }).json();

Per-entry options:

KeyValidates
requestThe json body before sending
responseThe parsed body when .json() is called
queryThe searchParams before sending
requestHeadersThe per-call headers (not merged client-level defaults)
responseHeadersThe response headers on the final attempt

Every entry accepts either a bare schema or { schema, severity: "error" | "warn" | "fatal" }. Default severity is "error".

schema: { query: { schema: QuerySchema, severity: "warn" }, // logs but doesn't fail }

Headers are normalized to Record<string, string> before validation — Headers instances and string[][] forms both work.

For structured, file-level API specifications covering every case of an endpoint (instead of inline per-call validation), use contract.http.with() instead.

Response methods

const res = await api.get("users"); await res.json<T>(); // Parse JSON body await res.text(); // Plain text await res.blob(); // Binary blob await res.arrayBuffer(); // Raw buffer

Retries

For flaky environments, you can retry selected status codes:

await api.get("status", { retry: { limit: 3, statusCodes: [429, 502, 503, 504], }, });

Auto-instrumentation

Every request made through a Glubean HTTP client automatically records:

  • An API trace via ctx.trace() (method, URL, status, duration)
  • A metric http_duration_ms via ctx.metric() (with method and path tags)

No manual instrumentation needed. Results appear in the Result Viewer and Cloud dashboard.

Next

Last updated on