Skip to content

[Feature] Custom response headers (response_headers field) #19

@ragilhadi

Description

@ragilhadi

Summary

All mock responses currently return only Content-Type: application/json. There is no way to set custom response headers, serve non-JSON content types, add CORS headers, set Location on redirects, or simulate Cache-Control / X-RateLimit-* headers.

Motivation

Real-world use cases that are blocked today:

  • CORS: Browser-based apps need Access-Control-Allow-Origin on every response
  • Redirects: 301 Moved Permanently requires a Location header
  • Non-JSON responses: Some APIs return XML, CSV, plain text, or HTML
  • Rate limit simulation: X-RateLimit-Remaining: 0, Retry-After: 60
  • Caching headers: Cache-Control: no-store, ETag: "abc123"
  • Auth challenges: WWW-Authenticate: Bearer realm="api"

Proposed Configuration

{
  "method": "GET",
  "path": "/data.xml",
  "status": 200,
  "response_headers": {
    "Content-Type": "application/xml; charset=utf-8",
    "Cache-Control": "no-cache",
    "X-Custom-Header": "my-value"
  },
  "response": "<users><user id=\"1\"/></users>"
}
{
  "method": "POST",
  "path": "/resources",
  "status": 201,
  "response_headers": {
    "Location": "/resources/99",
    "X-Request-Id": "abc-123"
  },
  "response": { "id": 99 }
}

Implementation Plan

src/types.rs

pub struct MockConfig {
    // existing fields ...

    /// Optional custom response headers
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub response_headers: Option<HashMap<String, String>>,
}

src/handler.rs

Replace the simple (status, Json(body)).into_response() with a Response::builder() approach:

let mut builder = Response::builder().status(status);

// Inject custom headers
if let Some(ref custom_headers) = mock.response_headers {
    for (name, value) in custom_headers {
        builder = builder.header(name, value);
    }
}

// Default Content-Type if not overridden
let has_content_type = mock.response_headers
    .as_ref()
    .map(|h| h.keys().any(|k| k.eq_ignore_ascii_case("content-type")))
    .unwrap_or(false);

if !has_content_type {
    builder = builder.header(CONTENT_TYPE, "application/json");
}

let body = serde_json::to_string(&mock.response).unwrap_or_default();
builder.body(Body::from(body)).unwrap().into_response()

Backward Compatibility

When response_headers is absent (the current default), behavior is identical to today — Content-Type: application/json is set automatically.

Acceptance Criteria

  • response_headers field added to MockConfig (optional)
  • All specified headers are present in the HTTP response
  • Default Content-Type: application/json is NOT added when response_headers contains a Content-Type
  • Header names are case-insensitive on input (normalize when setting)
  • Unit tests: custom headers present, Content-Type override, multiple headers
  • README updated with custom headers documentation and CORS example

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requesthandlerRequest handling

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions