How to make Requests in Hooks
Some Transactions need a resource that already exists. utils.request calls
another Transaction to create one. A request a hook makes this way is a Seed.
Addressing the call
Section titled “Addressing the call”utils.request is keyed by Selector — the same strings your hooks are
anchored to — and resolved through the same catalog, so the keys your editor
completes and the strings resolved at run time are the same strings by
construction.
utils.request( selector, // the Transaction to initiate args?, // body, headers, query, path, cookies — overlaid on the generated request options?, // runHooks, authorize);There is no forStatusCode: the Selector already carries the status you asked
for.
The Selector says what you asked for, not what you get
Section titled “The Selector says what you asked for, not what you get”A Selector names the Transaction you want to initiate. Whether it happens is
the server’s decision, so the answer is a union of every response the
operation declares — narrowed by statusCode, and by mediaType where one
status declares several.
import { beforeEach } from '@thymian/hooks';
export const seedLaunch = beforeEach( 'GET /launches/{id} -> 200 (application/json)', async (request, ctx, utils) => { const created = await utils.request( 'POST /launches (application/json) -> 201 (application/json)', { body: { missionName: 'Apollo 11' } }, { authorize: true }, );
if (created.statusCode !== 201) { utils.skip('could not create a launch to read back'); }
// Narrowed to the 201 body, so `id` is checked — and a field that stops // existing in the description is a compile error here. utils.setPathParam('id', created.body.id); },);Branching is how you get at a body: outside a narrowing check, created.body is
the union of every declared body, and only what they all have is readable.
Handling nothing is allowed
Section titled “Handling nothing is allowed”A hook that ignores the response still compiles and still runs. Nothing crashes, and nothing is silently wrong either — the run keeps its own account of what happened:
- If the Seed was answered with another declared status, the value comes
back truthfully and your hook carries on with it. If the Transaction then
cannot be executed — usually because the id you needed was never created —
thymian sampler checkreports it as skipped and names the Seed and the status it actually got. - If the Seed was answered with a status the description never declares,
utils.requestthrowsUndeclaredResponseError— there is no union member for it to be. You may catch it; if you do not, the Transaction is skipped, again naming the Seed, and the run continues.
↷ GET /launches/{id} -> 200 (application/json) The seed "POST /launches (application/json) -> 201 (application/json)" was answered with 500, which the specification does not declare for that operation. Branch on the seed's status code, or call utils.skip to end this transaction deliberately.To react to an off-spec answer yourself, catch it:
import { beforeEach, UndeclaredResponseError } from '@thymian/hooks';
export const seedLaunch = beforeEach( 'GET /launches/{id} -> 200 (application/json)', async (request, ctx, utils) => { try { const created = await utils.request( 'POST /launches (application/json) -> 201 (application/json)', { body: { missionName: 'Apollo 11' } }, );
utils.setPathParam('id', created.body.id); } catch (error) { if (!(error instanceof UndeclaredResponseError)) { throw error; }
utils.skip(`the API answered ${error.statusCode} while seeding`); } },);UndeclaredResponseError carries selector, statusCode, headers and the
parsed body, so an off-spec answer is inspectable rather than merely fatal.
Arguments overlay the generated request
Section titled “Arguments overlay the generated request”The generated sample plus the target’s own hooks already produce a complete
request. args is an overlay on it, so you write only what has to differ:
- Parameter groups merge per key. A header you set replaces that header; every other header the description produced survives.
- The body deep-merges. Objects recurse, arrays and primitives replace
whole, and
nullis a value rather than an absence.
// The generated body is { missionName: 'Generated', crew: ['A', 'B'], pad: { id: 1, name: 'LC-39A' } }await utils.request(selector, { body: { crew: ['Ada'], pad: { name: 'SLC-40' } },});// Sent: { missionName: 'Generated', crew: ['Ada'], pad: { id: 1, name: 'SLC-40' } }Everything is optional, so utils.request(selector) sends the generated request
unchanged.
The target’s own hooks run
Section titled “The target’s own hooks run”By default a nested request runs the target’s own
beforeEach → authorize → afterEach pipeline, so seeding behaves like the real
run rather than like a second, quieter client. Your authorize hook applies to
it; so does any beforeEach that shapes it. It is also sent to the origin the
run is talking to, so a --target-url covers your Seeds too.
await utils.request(selector, args, { runHooks: false });runHooks: false sends the generated request as-is. Use it when you want the raw
request, and when you need to break a cycle.
Cycles fail loudly
Section titled “Cycles fail loudly”Two hooks that seed each other would recurse until the stack gave out. Instead the sampler refuses, and prints the chain:
A cross-endpoint request would re-enter "GET /launches/{id} -> 200 (application/json)", which is already running.Try this: * The chain is: "GET /launches/{id} -> 200 (application/json)" → "POST /launches (application/json) -> 201 (application/json)" → "GET /launches/{id} -> 200 (application/json)" * Break the cycle, or pass `{ runHooks: false }` to seed from the raw request without running the target's hooks.The chain is printed because A → B → A is usually a mistake three files apart,
and the middle of it is where the fix goes.
Nothing is sent before the refusal.
Seeding once for the whole run
Section titled “Seeding once for the whole run”If one resource is shared by every Transaction, beforeAll is cheaper than
seeding per request — and its returned closure is how that resource gets cleaned
up.
import { beforeAll } from '@thymian/hooks';
export const seedOnce = beforeAll(async (utils) => { const created = await utils.request( 'POST /launches (application/json) -> 201 (application/json)', { body: { missionName: 'Shared launch' } }, { authorize: true }, );
if (created.statusCode !== 201) { // A throw in `beforeAll` ends the run: without this resource, nothing that // depends on it can be executed as described. throw new Error('the shared launch could not be created'); }
const id = created.body.id;
return async () => { await utils.request( 'DELETE /launches/{id} -> 204', { path: { id } }, { authorize: true }, ); };});