File-based Hooks Deep Dive
Understanding how the cascading hook system works internally and why it’s designed this way. In most cases, you will not need this guide to work with Thymian. But if you’re curious, keep reading!
Overview
Section titled “Overview”The hook system is the heart of the sampler plugin. It allows you to modify requests and responses through TypeScript/JavaScript functions that are automatically discovered based on their filesystem location.
This document explains the mechanics, execution model, and design rationale.
Hook Discovery
Section titled “Hook Discovery”File System Scanning
Section titled “File System Scanning”When sampler initializes, it:
-
Reads the directory structure from
.thymian/samples/(or any other configured directory) -
Scans each directory for hook files
-
Matches filenames against regex patterns:
/(.*\.)?beforeEach\.(ts|js|mjs|cjs|mts|cts)//(.*\.)?afterEach\.(ts|js|mjs|cjs|mts|cts)//(.*\.)?authorize\.(ts|js|mjs|cjs|mts|cts)/
-
Loads hooks dynamically using
jiti(Just-In-Time TypeScript/ESM loader) -
Builds a hook map indexed by transaction ID
Example Discovery
Section titled “Example Discovery”Given this structure:
DirectoryTodos
- auth.authorize.ts
Directory127.0.0.1
Directory3000
Directoryusers
- setup.beforeEach.ts
Directory@POST
- validate.afterEach.ts
Sampler discovers:
- 1 authorize hook (applies to all)
- 1 beforeEach hook (applies to
/users/*) - 1 afterEach hook (applies to
POST /users)
Hook Cascading
Section titled “Hook Cascading”The Tree Structure
Section titled “The Tree Structure”Hooks are associated with tree nodes that represent your API structure:
DirectoryRoot
DirectorySource (Todos)
DirectoryHost (127.0.0.1)
DirectoryPort (3000)
DirectoryPath (users)
DirectoryPathParameter ([id])
DirectoryMethod (@GET)
DirectoryStatusCode (200)
DirectoryResponseMediaType (application__json)
- Samples
Each node can have hooks attached.
Traversal Algorithm
Section titled “Traversal Algorithm”When preparing a request for transaction T:
- Start at root of the samples tree
- Traverse down to the specific samples node for
T - At each node, collect hooks attached to that node
- Merge hooks from parent into current (concatenation, not replacement)
- Result: A list of hooks ordered from root to leaf
Example Traversal:
For GET http://127.0.0.1:3000/users/123 -> 200:
1. Root node → collect root hooks2. Todos node → merge Todos hooks3. 127.0.0.1 node → merge host hooks4. 3000 node → merge port hooks5. users node → merge path hooks6. [id] node → merge parameter hooks7. @GET node → merge method hooks8. 200 node → merge status code hooks9. application__json node → merge media type hooks10. Samples node → final hook listHook Execution Order
Section titled “Hook Execution Order”BeforeEach Hooks
Section titled “BeforeEach Hooks”Execute before the HTTP request is sent, in order from root to leaf:
1. Root beforeEach hooks2. Source beforeEach hooks3. Host beforeEach hooks4. Port beforeEach hooks5. Path beforeEach hooks (for each path segment)6. PathParameter beforeEach hooks7. Method beforeEach hooks8. RequestMediaType beforeEach hooks9. StatusCode beforeEach hooks10. ResponseMediaType beforeEach hooksEach hook receives the modified request from the previous hook:
request0 (original) → hook1(request0) → request1 → hook2(request1) → request2 → hook3(request2) → request3 (final) → HTTP request sentAuthorize Hooks
Section titled “Authorize Hooks”Execute after beforeEach but before the request, in the same cascading order.
Special behavior:
- Only the last authorize hook in the chain executes
- Earlier authorize hooks are ignored
- This allows child endpoints to override parent authorization
AfterEach Hooks
Section titled “AfterEach Hooks”Execute after the HTTP response is received, in the same cascading order:
HTTP response received → hook1(response0) → response1 → hook2(response1) → response2 → hook3(response2) → response3 (final) → test results recordedHook Context
Section titled “Hook Context”What Hooks Receive
Section titled “What Hooks Receive”Each hook receives three arguments:
async (value, context, utils) => { // value: The thing to modify (request or response) // context: Transaction information // utils: Utility functions return modifiedValue;};For beforeEach and authorize:
type HttpRequestTemplate = { origin: string; path: string; method: string; headers: Record<string, unknown>; query: Record<string, unknown>; pathParameters: Record<string, unknown>; cookies: Record<string, unknown>; body?: unknown; authorize: boolean; bodyEncoding?: string;};For afterEach:
type HttpResponse = { statusCode: number; headers: Record<string, string | string[]>; body?: string; bodyEncoding?: string; trailers: Record<string, string>; duration: number;};Context
Section titled “Context”For beforeEach and authorize:
type ThymianHttpTransaction = { transactionId: string; thymianReq: ThymianHttpRequest; thymianRes: ThymianHttpResponse; transaction: HttpTransaction;};For afterEach:
type ContextForAfterEach = { requestTemplate: HttpRequestTemplate; request: HttpRequest; // Serialized request that was sent};See the Reference Section for the HookUtils API for more details.
Hook Loading
Section titled “Hook Loading”Dynamic Import
Section titled “Dynamic Import”Hooks are loaded using jiti (Just-In-Time loader):
const jiti = createJiti(import.meta.url);const hook = await jiti.import<BeforeEachRequestHook>(hookPath);This enables:
- TypeScript support without pre-compilation
- ESM and CommonJS compatibility
- Hot reloading during development
Import Validation
Section titled “Import Validation”After loading, sampler validates:
if (module === null || typeof module !== 'function') { throw new Error('Hook must be exported as default function');}Hooks must:
- Be exported as
export default - Be functions (async or sync)
- Match the expected type signature
Error Handling
Section titled “Error Handling”Hook Errors
Section titled “Hook Errors”If a hook throws an error:
try { result = await hook(value, context, utils);} catch (e) { if (e instanceof SkipError) { return { result: value, skip: e.message }; } if (e instanceof FailError) { return { result: value, fail: e.message }; } throw new ThymianBaseError(`Error in hook: ${e.message}`, { cause: e });}Special Errors:
SkipError(fromutils.skip()) → Skip test, continue with othersFailError(fromutils.fail()) → Fail test, continue with others- Other errors → Stop execution, report error
Hook Isolation
Section titled “Hook Isolation”Hooks are not isolated from each other:
- Module-level variables persist across hook invocations
- Errors in one hook don’t affect other endpoints
- Each transaction gets its own hook execution chain
Limitations
Section titled “Limitations”No Async Hook Discovery
Section titled “No Async Hook Discovery”Hooks are discovered synchronously at initialization. You cannot:
- Add hooks after initialization
- Conditionally load hooks based on runtime conditions
No Hook Priorities
Section titled “No Hook Priorities”Hooks execute in filesystem order (alphabetically by filename within a directory). You cannot:
- Explicitly set hook execution order
- Guarantee one hook runs before another at the same level
Workaround: Use filename prefixes:
01-first.beforeEach.ts02-second.beforeEach.ts03-third.beforeEach.tsNo Hook Deactivation
Section titled “No Hook Deactivation”Once loaded, hooks always run. You cannot:
- Disable a hook without deleting/renaming it
- Toggle hooks on/off via configuration