# APOPHIS v1.0 Extension Specification: Timeouts and Redirects
## Document Information
- **Version**: 1.0
- **Status**: Implemented
- **Scope**: APOPHIS v1.0 core extension
- **Date**: 2026-04-24
---
## Table of Contents
1. [Request Timeouts](#1-request-timeouts)
2. [Redirect Chains](#2-redirect-chains)
3. [APOSTL Formula Reference](#3-apostl-formula-reference)
4. [Integration Guide](#4-integration-guide)
---
## 1. Request Timeouts
### 1.1 Overview
Timeout support enables APOPHIS to detect slow endpoints and treat timeout violations as first-class contract violations. Timeouts are configurable at three levels (from highest to lowest precedence):
3.**No timeout**: Default behavior (no timeout enforced)
### 1.2 Configuration
#### Global Plugin Timeout
```typescript
awaitfastify.register(apophis,{
timeout: 5000,// 5 seconds for all routes
})
```
#### Per-Test Timeout
```typescript
constsuite=awaitfastify.apophis.contract({
timeout: 1000,// 1 second for this test run
})
```
#### Per-Route Timeout (Schema Annotation)
```typescript
fastify.get('/slow-endpoint',{
schema:{
'x-timeout':10000,// 10 seconds for this route
'x-ensures':[
'timeout_occurred(this) == false',
'response_code(this) == 200',
]
}
},async(request,reply)=>{
// Implementation
})
```
### 1.3 HTTP Executor Behavior
When a timeout is configured, `executeHttp` uses an abortable timer where supported. The timeout must be cleared in `finally`; Fastify injection may continue running after timeout if the underlying transport cannot be cancelled.
```typescript
// In src/infrastructure/http-executor.ts
if(timeoutMs&&timeoutMs>0){
response=awaitPromise.race([
fastify.inject(injectOptions),
newPromise<never>((_,reject)=>
setTimeout(()=>{
timedOut=true
reject(newError(`Request timeout after ${timeoutMs}ms`))
},timeoutMs)
),
])
}
```
On timeout, the executor returns a special `EvalContext` with:
| `timeout_value(this)` | Returns configured timeout in milliseconds, or `null` |
Example formulas:
```apostl
timeout_occurred(this) == false
timeout_value(this) == 5000
response_time(this) <= timeout_value(this)
```
### 1.5 Type Changes
#### `EvalContext` Extension
```typescript
exportinterfaceEvalContext{
// ... existing fields ...
timedOut?: boolean// True if request hit timeout
timeoutMs?: number// Configured timeout value
redirects?: RedirectEntry[]
}
```
#### `RouteContract` Extension
```typescript
exportinterfaceRouteContract{
// ... existing fields ...
timeout?: number// Per-route timeout in milliseconds
}
```
#### `TestConfig` Extension
```typescript
exportinterfaceTestConfig{
// ... existing fields ...
timeout?: number// Request timeout in milliseconds
}
```
---
## 2. Redirect Chains
### 2.1 Overview
Redirect support captures a 3xx response returned by `inject()` with its `Location` header. Multi-hop redirect following is not implemented here. When a response has:
- Status code 300-399
- A `location` header
APOPHIS captures the redirect entry in `EvalContext.redirects`.
### 2.2 HTTP Executor Behavior
After executing the request, `executeHttp` checks for redirects:
Note: Fastify injection returns the redirect response unless the caller implements redirect following. To test redirect behavior itself, assert the 3xx response and `location` header directly.
### 2.3 APOSTL Formulas
New operation headers for redirect inspection:
| Formula | Description |
|---------|-------------|
| `redirect_count(this)` | Returns number of redirect hops captured |
The timeout and redirect features integrate with the extension plugin system. Extensions can access timeout and redirect data via `PredicateContext.evalContext`:
```typescript
constmyExtension: ApophisExtension={
name:'timeout-monitor',
predicates:{
slow_endpoint:(ctx)=>({
value: ctx.evalContext.timedOut===true,
success: true,
}),
},
}
```
---
## Backward Compatibility
All timeout and redirect features are additive:
- Routes without `x-timeout` have no timeout enforced
- Routes without redirects have empty `redirects` array
- Formulas without timeout/redirect operations work unchanged