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 patch the responses MockForge generates from your OpenAPI spec, without editing the spec. Each rule picks operations with targets, then applies JSON patch operations to the response body.

Where rules come from

Rules are combined in this order, and all of them can be replaced at runtime:

  1. The overrides: list in mockforge.yaml.
  2. YAML files named by MOCKFORGE_HTTP_OVERRIDES_GLOB (comma-separated globs or absolute paths). Each file is a list of rules.
  3. A JSON array of rules in MOCKFORGE_HTTP_OVERRIDES. Hosted mocks receive their rules this way.
# mockforge.yaml
overrides:
  - name: Stamp a request id
    targets: ["operation:getUser"]
    patch:
      - op: add
        path: /requestId
        value: "{{uuid}}"
    post_templating: true

  - name: Fail payments
    targets: ["tag:Payments"]
    patch:
      - op: replace
        path: /status
        value: FAILED

Targets

A rule applies when any of its targets matches the operation.

TargetMatches
operation:getUserThe operation whose operationId is getUser
tag:UsersOperations tagged Users in the spec
path:^/users/Path templates matching the regex, such as /users/{id}
regex:^listOperation IDs matching the regex
*Every operation

Patch operations

path is a JSON pointer into the response body. Use "" for the whole body.

patch:
  - op: add          # add or set a field
    path: /metadata/source
    value: mock
  - op: replace      # replace an existing value
    path: /user/tier
    value: gold
  - op: remove       # delete a field
    path: /debug

With mode: merge, add and replace deep-merge objects into the existing value instead of overwriting it. The default is mode: replace.

Conditions

when limits a rule to matching requests. Supported forms:

ConditionTrue when
header[x-scenario]=emptyThe request header equals the value (names are case-insensitive)
header[x-scenario]!=emptyThe header differs from the value
query[format]=detailedThe query parameter equals the value
$.request.body.plan == 'pro'A JSONPath into the request body compares equal
AND(a, b), OR(a, b), NOT(a)Combinations of the above
overrides:
  - name: Empty list for demos
    targets: ["path:^/users$"]
    when: "header[x-scenario]=empty"
    patch:
      - op: replace
        path: ""
        value: []

Templating

Token values such as {{uuid}} in rule files are expanded once, when the file loads. Set post_templating: true to expand tokens in the patched body on every response instead.

Turning rules off

Set enabled: false to keep a rule without applying it. The admin UI’s Overrides page toggles this for you.

Changing rules at runtime

With --admin, the admin server exposes the live rule set:

# Read the rules
curl http://localhost:9080/__mockforge/overrides

# Replace them all; the next response uses the new rules
curl -X PUT http://localhost:9080/__mockforge/overrides \
  -H 'content-type: application/json' \
  -d '{"rules": [{"targets": ["operation:ping"], "patch": [{"op": "add", "path": "/overridden", "value": true}]}]}'

A PUT is validated as a whole. An invalid target regex or JSON pointer returns 400 with the reason, and the previous rules stay in place. Runtime changes are not written back to mockforge.yaml.

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

  1. Resource Limits: Set appropriate memory and CPU limits for plugins
  2. Error Handling: Implement proper error handling and logging
  3. Testing: Thoroughly test plugins with various inputs
  4. Documentation: Document plugin capabilities and configuration options

Override Usage

  1. Selective Application: Use specific targets to avoid unintended modifications
  2. Version Control: Keep override configurations in version control
  3. Testing: Test overrides with different request scenarios
  4. Performance: Minimize complex conditions and patch operations

Response Design

  1. Consistency: Maintain consistent response formats across endpoints
  2. Error Details: Provide meaningful error messages and codes
  3. Metadata: Include relevant metadata like timestamps and request IDs
  4. Content Types: Set appropriate Content-Type headers

Security Considerations

  1. Input Validation: Validate all inputs in custom plugins
  2. Resource Limits: Prevent resource exhaustion attacks
  3. Authentication: Implement proper authentication for sensitive endpoints
  4. 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.