Custom Responses
MockForge provides multiple powerful ways to create custom HTTP responses beyond basic OpenAPI schema generation. This guide covers advanced response customization techniques including plugins, overrides, and dynamic generation.
Response Override Rules
Override rules allow you to modify OpenAPI-generated responses using JSON patches without changing the original specification.
Basic Override Configuration
# mockforge.yaml
http:
openapi_spec: api-spec.json
response_template_expand: true
# Override specific endpoints
overrides:
- targets: ["path:/users"]
patch:
- op: replace
path: "/responses/200/content/application~1json/example"
value:
users:
- id: "{{uuid}}"
name: "John Doe"
email: "john@example.com"
- id: "{{uuid}}"
name: "Jane Smith"
email: "jane@example.com"
- targets: ["operation:getUser"]
patch:
- op: add
path: "/responses/200/content/application~1json/example/profile"
value:
avatar: "https://example.com/avatar.jpg"
bio: "User biography"
Override Targeting
Target specific operations using different selectors:
overrides:
# By operation ID
- targets: ["operation:listUsers", "operation:createUser"]
patch: [...]
# By path pattern
- targets: ["path:/users/*"]
patch: [...]
# By tag
- targets: ["tag:Users"]
patch: [...]
# By regex
- targets: ["regex:^/api/v[0-9]+/users$"]
patch: [...]
Patch Operations
Supported JSON patch operations:
overrides:
- targets: ["path:/users"]
patch:
# Add new fields
- op: add
path: "/responses/200/content/application~1json/example/metadata"
value:
total: 100
page: 1
# Replace existing values
- op: replace
path: "/responses/200/content/application~1json/example/users/0/name"
value: "Updated Name"
# Remove fields
- op: remove
path: "/responses/200/content/application~1json/example/users/1/email"
# Copy values
- op: copy
from: "/responses/200/content/application~1json/example/users/0/id"
path: "/responses/200/content/application~1json/example/primaryUserId"
# Move values
- op: move
from: "/responses/200/content/application~1json/example/temp"
path: "/responses/200/content/application~1json/example/permanent"
Conditional Overrides
Apply overrides based on request conditions:
overrides:
- targets: ["path:/users"]
when: "request.query.format == 'detailed'"
patch:
- op: add
path: "/responses/200/content/application~1json/example/users/0/profile"
value:
bio: "Detailed user profile"
preferences: {}
- targets: ["path:/users"]
when: "request.header.X-API-Version == 'v2'"
patch:
- op: add
path: "/responses/200/content/application~1json/example/apiVersion"
value: "v2"
Override Modes
Control how patches are applied:
overrides:
# Replace mode (default) - complete replacement
- targets: ["path:/users"]
mode: replace
patch: [...]
# Merge mode - deep merge objects and arrays
- targets: ["path:/users"]
mode: merge
patch:
- op: add
path: "/responses/200/content/application~1json/example"
value:
additionalField: "value"
Response Plugins
Create custom response generation logic using MockForge’s plugin system.
Response Generator Plugin
Implement the ResponsePlugin trait for complete response control:
#![allow(unused)] fn main() { use mockforge_plugin_core::*; pub struct CustomResponsePlugin; #[async_trait::async_trait] impl ResponsePlugin for CustomResponsePlugin { fn capabilities(&self) -> PluginCapabilities { PluginCapabilities { network: NetworkCapabilities { allow_http_outbound: true, allowed_hosts: vec!["api.example.com".to_string()], }, filesystem: FilesystemCapabilities::default(), resources: PluginResources { max_memory_bytes: 50 * 1024 * 1024, max_cpu_time_ms: 5000, }, custom: HashMap::new(), } } async fn initialize(&self, config: &ResponsePluginConfig) -> Result<()> { // Plugin initialization Ok(()) } async fn can_handle( &self, _context: &PluginContext, request: &ResponseRequest, _config: &ResponsePluginConfig, ) -> Result<PluginResult<bool>> { // Check if this plugin should handle the request let should_handle = request.path.starts_with("/api/custom/"); Ok(PluginResult::success(should_handle, 0)) } async fn generate_response( &self, _context: &PluginContext, request: &ResponseRequest, _config: &ResponsePluginConfig, ) -> Result<PluginResult<ResponseData>> { // Generate custom response match request.path.as_str() { "/api/custom/weather" => { let weather_data = serde_json::json!({ "temperature": 22, "condition": "sunny", "location": request.query_param("location").unwrap_or("Unknown") }); Ok(PluginResult::success( ResponseData::json(200, &weather_data)?, 0 )) } "/api/custom/time" => { let time_data = serde_json::json!({ "current_time": chrono::Utc::now().to_rfc3339(), "timezone": request.query_param("tz").unwrap_or("UTC") }); Ok(PluginResult::success( ResponseData::json(200, &time_data)?, 0 )) } _ => Ok(PluginResult::success( ResponseData::not_found("Custom endpoint not found"), 0 )) } } fn priority(&self) -> i32 { 100 } fn validate_config(&self, _config: &ResponsePluginConfig) -> Result<()> { Ok(()) } fn supported_content_types(&self) -> Vec<String> { vec!["application/json".to_string()] } } }
Plugin Configuration
Configure response plugins in your MockForge setup:
# plugin.yaml
name: custom-response-plugin
version: "1.0.0"
type: response
config:
enabled: true
priority: 100
content_types:
- "application/json"
url_patterns:
- "/api/custom/*"
methods:
- "GET"
- "POST"
settings:
external_api_timeout: 5000
cache_enabled: true
Response Modifier Plugin
Modify responses after generation using the ResponseModifierPlugin trait:
#![allow(unused)] fn main() { use mockforge_plugin_core::*; pub struct ResponseModifierPlugin; #[async_trait::async_trait] impl ResponseModifierPlugin for ResponseModifierPlugin { fn capabilities(&self) -> PluginCapabilities { PluginCapabilities::default() } async fn initialize(&self, _config: &ResponseModifierConfig) -> Result<()> { Ok(()) } async fn should_modify( &self, _context: &PluginContext, _request: &ResponseRequest, response: &ResponseData, _config: &ResponseModifierConfig, ) -> Result<PluginResult<bool>> { // Modify successful JSON responses let should_modify = response.status_code == 200 && response.content_type == "application/json"; Ok(PluginResult::success(should_modify, 0)) } async fn modify_response( &self, _context: &PluginContext, _request: &ResponseRequest, mut response: ResponseData, _config: &ResponseModifierConfig, ) -> Result<PluginResult<ResponseData>> { // Add custom headers response.headers.insert( "X-Custom-Header".to_string(), "Modified by plugin".to_string() ); // Add metadata to JSON responses if let Some(json_str) = response.body_as_string() { if let Ok(mut json_value) = serde_json::from_str::<serde_json::Value>(&json_str) { if let Some(obj) = json_value.as_object_mut() { obj.insert("_metadata".to_string(), serde_json::json!({ "modified_by": "ResponseModifierPlugin", "timestamp": chrono::Utc::now().timestamp() })); } let modified_body = serde_json::to_vec(&json_value) .map_err(|e| PluginError::execution(format!("JSON serialization error: {}", e)))?; response.body = modified_body; } } Ok(PluginResult::success(response, 0)) } fn priority(&self) -> i32 { 50 } fn validate_config(&self, _config: &ResponseModifierConfig) -> Result<()> { Ok(()) } } }
Template Plugins
Extend MockForge’s templating system with custom functions.
Custom Template Functions
#![allow(unused)] fn main() { use mockforge_plugin_core::*; pub struct BusinessTemplatePlugin; impl TemplatePlugin for BusinessTemplatePlugin { fn execute_function( &mut self, function_name: &str, args: &[TemplateArg], _context: &PluginContext, ) -> PluginResult<String> { match function_name { "business_id" => { // Generate business-specific ID let id = format!("BIZ-{:010}", rand::random::<u32>()); PluginResult::success(id, 0) } "department_name" => { // Generate department name let departments = ["Engineering", "Sales", "Marketing", "HR", "Finance"]; let dept = departments[rand::random::<usize>() % departments.len()]; PluginResult::success(dept.to_string(), 0) } "employee_data" => { // Generate complete employee object let employee = serde_json::json!({ "id": format!("EMP-{:06}", rand::random::<u32>() % 1000000), "name": "{{faker.name}}", "department": "{{department_name}}", "salary": rand::random::<u32>() % 50000 + 50000, "hire_date": "{{faker.date.past 365}}" }); PluginResult::success(employee.to_string(), 0) } _ => PluginResult::failure( format!("Unknown function: {}", function_name), 0 ) } } fn get_available_functions(&self) -> Vec<TemplateFunction> { vec![ TemplateFunction { name: "business_id".to_string(), description: "Generate a business ID".to_string(), args: vec![], return_type: "string".to_string(), }, TemplateFunction { name: "department_name".to_string(), description: "Generate a department name".to_string(), args: vec![], return_type: "string".to_string(), }, TemplateFunction { name: "employee_data".to_string(), description: "Generate complete employee data".to_string(), args: vec![], return_type: "json".to_string(), }, ] } fn get_capabilities(&self) -> PluginCapabilities { PluginCapabilities::default() } fn health_check(&self) -> PluginHealth { PluginHealth::healthy("Template plugin healthy".to_string(), PluginMetrics::default()) } } }
Using Custom Templates
# OpenAPI spec with custom templates
paths:
/employees:
get:
responses:
'200':
content:
application/json:
example:
employees:
- "{{employee_data}}"
- "{{employee_data}}"
business_id: "{{business_id}}"
Configuration-Based Custom Responses
Define custom responses directly in configuration files.
Route-Specific Responses
# mockforge.yaml
http:
port: 3000
routes:
- path: /api/custom/dashboard
method: GET
response:
status: 200
headers:
Content-Type: application/json
X-Custom-Header: Dashboard-Data
body: |
{
"widgets": [
{
"id": "sales-chart",
"type": "chart",
"data": [120, 150, 180, 200, 250]
},
{
"id": "user-stats",
"type": "stats",
"data": {
"total_users": 15420,
"active_users": 8920,
"new_signups": 245
}
}
],
"last_updated": "{{now}}"
}
- path: /api/custom/report
method: POST
response:
status: 201
headers:
Location: /api/reports/123
body: |
{
"report_id": "RPT-{{randInt 1000 9999}}",
"status": "processing",
"estimated_completion": "{{now+5m}}"
}
Dynamic Route Matching
routes:
# Path parameters
- path: /api/users/{userId}/profile
method: GET
response:
status: 200
body: |
{
"user_id": "{{request.path.userId}}",
"name": "{{faker.name}}",
"email": "{{faker.email}}",
"profile": {
"bio": "{{faker.sentence}}",
"location": "{{faker.city}}, {{faker.country}}"
}
}
# Query parameter conditions
- path: /api/search
method: GET
response:
status: 200
body: |
{{#if (eq request.query.type 'users')}}
{
"results": [
{"id": 1, "name": "John", "type": "user"},
{"id": 2, "name": "Jane", "type": "user"}
]
}
{{else if (eq request.query.type 'posts')}}
{
"results": [
{"id": 1, "title": "Post 1", "type": "post"},
{"id": 2, "title": "Post 2", "type": "post"}
]
}
{{else}}
{
"results": [],
"message": "No results found for type: {{request.query.type}}"
}
{{/if}}
Error Response Customization
Create sophisticated error responses for different scenarios.
Structured Error Responses
routes:
- path: /api/users/{userId}
method: GET
response:
status: 404
headers:
Content-Type: application/json
body: |
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID {{request.path.userId}} not found",
"details": {
"user_id": "{{request.path.userId}}",
"requested_at": "{{now}}",
"request_id": "{{uuid}}"
},
"suggestions": [
"Check if the user ID is correct",
"Verify the user exists in the system",
"Try searching by email instead"
]
}
}
- path: /api/orders
method: POST
response:
status: 422
body: |
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"validation_errors": [
{
"field": "customer_email",
"code": "invalid_format",
"message": "Email format is invalid"
},
{
"field": "order_items",
"code": "min_items",
"message": "At least one order item is required"
}
]
}
}
Conditional Error Responses
routes:
- path: /api/payments
method: POST
response:
status: 402
condition: "request.header.X-Test-Mode == 'insufficient_funds'"
body: |
{
"error": "INSUFFICIENT_FUNDS",
"message": "Payment failed due to insufficient funds",
"details": {
"available_balance": 50.00,
"requested_amount": 100.00,
"currency": "USD"
}
}
- path: /api/payments
method: POST
response:
status: 500
condition: "request.header.X-Test-Mode == 'server_error'"
body: |
{
"error": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred while processing payment",
"reference_id": "ERR-{{randInt 100000 999999}}",
"timestamp": "{{now}}"
}
Advanced Response Features
Response Delays and Latency
routes:
- path: /api/slow-endpoint
method: GET
response:
status: 200
delay_ms: 2000 # 2 second delay
body: |
{
"message": "This response was delayed",
"timestamp": "{{now}}"
}
- path: /api/variable-delay
method: GET
response:
status: 200
delay_ms: "{{randInt 100 5000}}" # Random delay between 100ms-5s
body: |
{
"message": "Random delay applied",
"delay_applied_ms": "{{_delay_ms}}"
}
Response Caching
routes:
- path: /api/cached-data
method: GET
response:
status: 200
headers:
Cache-Control: max-age=300
X-Cache-Status: "{{_cache_hit ? 'HIT' : 'MISS'}}"
cache: true
cache_ttl_seconds: 300
body: |
{
"data": "This response may be cached",
"generated_at": "{{now}}",
"cache_expires_at": "{{now+5m}}"
}
Binary Response Handling
routes:
- path: /api/download/{filename}
method: GET
response:
status: 200
headers:
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="{{request.path.filename}}"
body_file: "/path/to/binary/files/{{request.path.filename}}"
- path: /api/images/{imageId}
method: GET
response:
status: 200
headers:
Content-Type: image/png
Cache-Control: max-age=3600
body_base64: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
Testing Custom Responses
Manual Testing
# Test custom route
curl http://localhost:3000/api/custom/dashboard
# Test with parameters
curl "http://localhost:3000/api/users/123/profile"
# Test error conditions
curl -H "X-Test-Mode: insufficient_funds" \
http://localhost:3000/api/payments \
-X POST \
-d '{}'
Automated Testing
#!/bin/bash
# test-custom-responses.sh
BASE_URL="http://localhost:3000"
echo "Testing custom responses..."
# Test dashboard endpoint
DASHBOARD_RESPONSE=$(curl -s $BASE_URL/api/custom/dashboard)
echo "Dashboard response:"
echo $DASHBOARD_RESPONSE | jq '.'
# Test user profile with path parameter
USER_RESPONSE=$(curl -s $BASE_URL/api/users/456/profile)
echo "User profile response:"
echo $USER_RESPONSE | jq '.'
# Test error responses
ERROR_RESPONSE=$(curl -s -H "X-Test-Mode: insufficient_funds" \
-X POST \
-d '{}' \
$BASE_URL/api/payments)
echo "Error response:"
echo $ERROR_RESPONSE | jq '.'
echo "Custom response tests completed!"
Best Practices
Plugin Development
- Resource Limits: Set appropriate memory and CPU limits for plugins
- Error Handling: Implement proper error handling and logging
- Testing: Thoroughly test plugins with various inputs
- Documentation: Document plugin capabilities and configuration options
Override Usage
- Selective Application: Use specific targets to avoid unintended modifications
- Version Control: Keep override configurations in version control
- Testing: Test overrides with different request scenarios
- Performance: Minimize complex conditions and patch operations
Response Design
- Consistency: Maintain consistent response formats across endpoints
- Error Details: Provide meaningful error messages and codes
- Metadata: Include relevant metadata like timestamps and request IDs
- Content Types: Set appropriate Content-Type headers
Security Considerations
- Input Validation: Validate all inputs in custom plugins
- Resource Limits: Prevent resource exhaustion attacks
- Authentication: Implement proper authentication for sensitive endpoints
- Logging: Log security-relevant events without exposing sensitive data
Troubleshooting
Plugin Issues
Plugin not loading: Check plugin configuration and file paths Plugin timeout: Increase resource limits or optimize plugin code Plugin errors: Check plugin logs and error messages
Override Problems
Overrides not applying: Verify target selectors and patch syntax JSON patch errors: Validate patch operations against JSON structure Condition evaluation: Test conditional expressions with sample requests
Performance Issues
Slow responses: Profile plugin execution and optimize bottlenecks Memory usage: Monitor plugin memory consumption and adjust limits Template expansion: Simplify complex templates or use static responses
For basic HTTP mocking features, see the HTTP Mocking guide. For advanced templating, see the Dynamic Data guide.