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
prefixUrl—api.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 setsthrowHttpErrors: falseso you always get a response object back. Usectx.expect(res).toHaveStatus(404)to verify error status codes. - Empty
searchParamsdon’t pollute URLs — passingsearchParams: {}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
| Option | Type | Description |
|---|---|---|
json | object | JSON body (auto-serialized) |
body | FormData | string | Raw body |
searchParams | Record<string, string> | Query string |
headers | Record<string, string> | Request headers |
timeout | number | false | Timeout in ms (default 10000, false to disable) |
retry | number | object | Retry count or { limit, statusCodes, methods } |
throwHttpErrors | boolean | Throw on 4xx/5xx (default false) |
schema | HttpSchemaOptions | Per-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:
| Key | Validates |
|---|---|
request | The json body before sending |
response | The parsed body when .json() is called |
query | The searchParams before sending |
requestHeaders | The per-call headers (not merged client-level defaults) |
responseHeaders | The 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 bufferRetries
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_msviactx.metric()(with method and path tags)
No manual instrumentation needed. Results appear in the Result Viewer and Cloud dashboard.