Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API mock (apimock-rs) Documentation

A developer-friendly, robust and functional HTTP mock server built in Rust.

While this is a CLI tool, a featherlight GUI wrapper named API mokka will be also available. (development in progress)

Who is this for?

  • Developers who want to quickly mock APIs without heavy setup.
  • Beginners who benefit from minimal configuration.
  • Advanced users needing logic-based response behavior.

Quick Start

npm install -D apimock-rs
npx apimock

demo

For Users


Contribution

API mock (apimock-rs) Users Documentation

This guide walks through how to get started with apimock-rs, a mock HTTP server for testing APIs. It covers basic setup, examples, frequently asked questions, deep configuration.

🏞️ Concept Overview

API mock (apimock-rs) is a developer-friendly, lightweight and functional HTTP mock server built in Rust. It provides file-based mechanism for mocking RESTful APIs using static JSON responses. Optionally, dynamic routing is supported: rule-based matching via toml and scripting via rhai.

Key Features

  • 👟 Zero-config start – Just point to a directory (folder) with JSON files and run.
  • 🍬 Simple setup – Usable via a single CLI command, no compilation needed via npm-distributed binary.
  • 🧳 Static routing – File-based simple responses. Uses directory paths and .json-like files to simulate API endpoints.
  • 🎒 Dynamic routing with matching – Supports conditional responses with rule-based mechanism and scripting.
  • 🍨 High-performance – Effortless speed and minimal presence. Built with async Rust using tokio and hyper.

Getting started

It only takes a few steps to get your mock server up and running !

System Requirements

  • Node.js and npm (stable) only
    • (Optional) Without them, natively built executable is also available

Installation

If not installed yet:

npm install -D apimock-rs

Note that the package name is apimock-rs (ends with -rs) and its command name below is apimock.

Minimal Configuration

Nothing. Just JSON and go !

Running the Server

npx apimock

Besides, if you use natively built executable, run ./apimock instead.

Test it

From another terminal (as web client), access the server:

curl -i http://localhost:3001/

Expected response: HTTP Status Code 404 (NOT FOUND)

This is correct, as no .json file exists on the server yet. Now, let's prepare for the next test. In the server terminal, run:

echo '{"hello": "world"}' > greetings.json

npx apimock

Then, access it again with:

curl http://localhost:3000/greetings

Expected response:

{
  "hello": "world"
}

All set 😺 ?

File-based routing

The server uses your directory structure to automatically serve the correct response. When a request comes in, it checks your directories and files (like data.json for /data) to find a match for the URL. All you need to do is organize your files and directories where the server is running.

Example Directory Structure

Here's an example of how you might set up:

(root)/
└── api/
    └── v1/
        ├── status.json
        ├── user/
        │   ├── 1.json
        │   └── 2.json5
        └── users.csv
  • A request to /api/v1/status returns status.json.
  • /api/v1/user/1 returns the content of 1.json.
  • /api/v1/user/2 returns the content of 2.json5. .json5 is equivalent to .json to the server.
  • /api/v1/users returns the content of users.csv as list.
  • Each of /, /api and /api/v1 returns HTTP Status Code 404
    • unless an "index" file (e.g., index.json, .json5, or .csv. Also .html) is present in the respective directory.

What's next ?

File-based routing is great for simple cases where your response directly maps to a URL. It gets you up and running quickly for many basic mocking needs.

Expanding Your Mock Server's Capabilities

However, you'll quickly discover some limitations. For instance, you can't create responses that depend on request headers or body data, limiting your flexibility.

When your mocking needs go beyond basic URL matching, rule-based configuration becomes incredibly powerful. This approach gives you much more control, but it requires a configuration file. Don't worry, we'll walk you through setting it up so you can harness its full potential !

Root configuration

Your first configuration files

Let's create your configuration files. You don't need to open a text editor just yet ! Instead, use the --init argument with the npx apimock command:

npx apimock --init

After running this, you'll see "created" printed in your terminal, and you'll find two new configuration files in your project:

  • apimock.toml
  • apimock-rule-set.toml

Running the Server with Configuration

Now that you have your configuration files, let's try running the server !

npx apimock

You'll notice a line in the terminal like: "@ rule_set #1 (./apimock-rule-set.toml)". This indicates that the example rule set is now active and routing requests.

Moving Configuration Files

You have the flexibility to move these configuration files. For example, if you place them in a tests/ directory:

your_project/
├── tests/
│   ├── apimock.toml
│   └── apimock-rule-set.toml
└── ...

You can tell the server where to find your apimock.toml file using either the -c or --config argument:

npx apimock -c tests/apimock.toml

Important: If apimock.toml references apimock-rule-set.toml (perhaps by default), the rule set path is interpreted relative to apimock.toml itself, not from your current working directory. Keep this in mind when organizing your files.

You're now ready to define powerful, rule-based dynamic routing !

Rule-based routing

Defining Rules

Now, let's dive into apimock-rule-set.toml and define some powerful rules ! Each rule checks incoming requests against specific conditions (like URL path, headers, or body content) and, if they match, sends a predefined response.

Here are examples of how you can set up different types of rules in apimock-rule-set.toml:

Example 1: Match with Request URL Path

These examples show how to define responses based on the incoming request's URL path.

# apimock-rule-set.toml
[[rules]]
when.request.url_path = ""
respond = { text = "I'm at root." }
# apimock-rule-set.toml
[[rules]]
when.request.url_path = "home"
# Make sure to create `home.json` in a JSON format!
respond.file_path = "home.json"

Test:

curl http://localhost:3001/
# I'm at root.

curl http://localhost:3001/home
# (home.json content)

Rule-based routing: More practices

In addition to URL path, you can match with HTTP method, headers and body JSON data.

Example 2: Match with HTTP Method

# apimock-rule-set.toml
[[rules]]
when.request.method = "GET"
respond = { status = 403 }

[[rules]]
when.request.method = "POST"
respond = { status = 200 }

Test:

curl -i -X GET http://localhost:3001/
# 403 Forbidden

curl -i -X POST http://localhost:3001/
# 200 OK

Example 3: Match with Headers

This example demonstrates how to match requests based on specific HTTP headers. This is useful for simulating authentication or content negotiation.

# apimock-rule-set.toml
[[rules]]
[rules.when.request.headers]
Authorization = { value = "Bearer eyJhb", op = "starts_with" }
[rules.respond]
text = "Authorized !"

Test:

curl -H "Authorization: Bearer eyJhb(...).(...).(...)" http://localhost:3001/
# Authorized !

Example 4: Match with Body JSON data by Dot-Notation JSON Path

This powerful feature lets you match requests based on specific values within the JSON body of an incoming request. You define the target key using a dot-notation path.

# apimock-rule-set.toml
[[rules]]
[rules.when.request.body.json]
"a.b.c" = { value = "d" }
[rules.respond]
text = "Body JSON matched !"

Test:

curl http://localhost:3001/ \
    -H "Content-Type: application/json" \
    -d '{"a":{"b":{"c":"d"}}}'
# Body JSON matched !

Understanding TOML Configuration

Your apimock.toml and apimock-rule-set.toml files use TOML (Tom's Obvious, Minimal Language). If you're new to TOML, don't worry ! It's designed to be human-readable and easy to learn. Here are the essentials you'll need to know:

Basic Syntax

Key-Value Pairs

The most fundamental part.

key = "value"
number_key = 123
boolean_key = true

Comments

Use the hash symbol (#) to add comments. Anything from # to the end of the line is ignored by the parser.

# This is a full-line comment
key = "value" # This is an end-of-line comment

Tables (Dictionaries)

TOML uses tables (similar to dictionaries or objects in other languages) to group related key-value pairs.

Inline Tables

For compact, small tables.

user = { name = "Alice", age = 30 }

Standard Tables

Defined with [table-name].

[listener]
ip_address = "127.0.0.1"
port = 8080

Nested Tables

You can define tables within tables using dot notation.

[rules.when.request.headers]
user = { value = "user1" }

Key Naming Flexibility

You can use hyphens (-) in key names, not just underscores (_). While you can enclose key names in quotes, it's often not necessary unless the key contains special characters or needs to start with a number.

api-key = "my-secret-token"
# Same as above, quotes are optional here
"api-key" = "my-secret-token"

# quotes are required here because the key contains the special character `.`
"a.b.c" = { value = "d" }

Array of Tables (Lists of Dictionaries)

This is crucial for defining your rules ! Arrays of tables are used to create a list of similar objects. Each [[table-name]] defines a new item in the list.

[[rules]] # First rule in the list
when.request.url_path = "/home"
respond.text = "Hello, world"

[[rules]] # Second rule in the list
when.request.url_path = "/"
respond.text = "I'm at root"

For more in-depth learning

You can refer to the official TOML specification or other popular guides.

Examples

Real-World Rule-Based Routing

Now that you've got the basics down, let's explore how to use rule-based configuration to meet more realistic, real-world mocking needs. These examples will help you handle complex scenarios and make your mock server incredibly flexible.

Combining Conditions

Example

Here's an example where we combine a URL path with two HTTP headers as conditions:

[[rules]]
[rules.when.request]
url_path = "/api/check"
[rules.when.request.headers]
User = { value = "user1" }
X-Request-Id = { value = "abc123" }

[rules.respond]
text = "Matched with headers"

Important Note on Combined Conditions

These conditions are evaluated using AND logic. This means all defined conditions within a single rule must be true for the rule to match the incoming request. If even one condition isn't met, the matching fails for that particular rule.

Combining Conditions: Powerful Matching

You can combine conditions in a few ways

  • Across Different Request Parts: Define multiple conditions involving the URL path, HTTP headers, and JSON body within a single rule. All specified conditions must be met for the rule to trigger.
  • Multiple Conditions within Headers: Specify multiple header key-value pairs that all must be present and match.
  • Multiple Conditions within Body JSON: Define multiple JSONPath conditions within the request body that all must match.

Combining conditions gives you tighter control over when a mock response is returned, enabling you to simulate complex API behaviors with precision.

Example

Here's an example where we combine a URL path with two HTTP headers and a body JSON Path as conditions:

# rule No.1 (priority)
[[rules]]
[rules.when.request]
url_path = "/api/check"
[rules.when.request.headers]
User = { value = "user1" }
X-Request-Id = { value = "abc123" }
[rules.when.request.body.json]
"a.b.c" = { value = "d" }

[rules.respond]
# Make sure to create `strictly-matched.json` in a JSON format!
file_path = "strictly-matched.json"

# rule No.2
[[rules]]
when.request.url_path = "/api/check"
when.request.headers.User = { value = "user1" }

[rules.respond]
text = "matched"

Important Note on Rule Order

The mock server uses a first-match strategy. This means it checks your rules from top to bottom in your apimock-rule-set.toml file. The first rule that completely matches an incoming request will be applied, and no further rules will be checked. Therefore, place your most specific or highest-priority rules at the top of the file to ensure they are evaluated first.

Using Operators for Flexible Matching

So far, our rule conditions have only used exact (equal) matching. For instance, a header had to be exactly "Bearer xyz", or a path had to be exactly "/api/users".

But real-world scenarios often require more flexibility. Your mock server provides powerful operators that allow you to define various types of matching behavior:

  • equal: (Default) Matches if the value is exactly the same.
  • not_equal: Matches if the value is not exactly the same.
    • Be careful ! Because it matches anything other than the specified value, it can often lead to broader matches than you intend. Consider if a more specific equal or other operator might be better for your use case.
  • starts_with: Matches if the value begins with a specific string.
  • contains: Matches if the value includes a specific string anywhere within it.
  • wild_card: Matches using simple wildcard patterns (* for any sequence of characters, ? for any single character). This is incredibly versatile for dynamic paths or values.
    • Recommended for experienced users only. The wild_card operator offers immense flexibility, but it's easy to create unintended matches. Use it with caution and test your rules thoroughly to ensure they behave as expected.

By choosing the right operator, you can define rules that are both precise and adaptable to varying request patterns.

Example

[[rules]]
when.request.url_path = { value = "/disallowed", op = "starts_with" }
respond.status = 403 # FORBIDDEN
# you can't access both `http://localhost:3001/disallowed` and `http://localhost:3001/disallowed/some/room` (403 Forbidden) but can `http://localhost:3001/allowed/disallowed` (perhaps 404 Not Found)

[[rules]]
when.request.url_path = { value = "cookie", op = "contains" }
respond.text = "Cookie found !"
# test with `curl http://localhost:3001/delicious/cookie/in-the-can`

FAQ (Frequently Asked Questions)

File-based routing

Q: Can I start using the app without extensive initial setup?
A: Yes, absolutely ! You can get the mock server running quickly. Just run npm install -D apimock-rs followed by npx apimock (or execute the binary file after downloading it).

The directory where you run the command will be treated as the root directory for the server, and it will respond to HTTP requests by mapping the request paths to the relative paths of files (like .json files) within the root. The detail about File-based routing is here.

Q: Can the server return responses for HTTP request paths that are directories, not specific files?
A: Yes. For requests like /api/v1/myfunction (without a file extension), rather than /api/v1/my.json, you can use an "index" file to provide a response.

If an index.json, .json5, .csv, or .html file exists within that directory, it will be served as the response for directory access. If none of the index files are present, the server will return 404 NOT FOUND.

Rule-based routing

Q: I want to map specific HTTP requests to individual responses. Is this possible?
A: While not achievable with file-based routing alone, you can define custom rules in a configuration file to achieve it. How to create it is written below and the detail about Rule-based routing is here.

Q: How do I create a configuration file?
A: Simply run npx apimock --init. This command will generate a configuration file set in the current directory. You can then edit apimock-rule-set.toml to customize your routing rules.

Q: Which option is supported as matching condition ?
A: You can use url_path, method, headers and body.json as conditions, which can be used both alone and combined with each other.

Response via script

Q: Can I dynamically generate responses ?
A: Yes, partially supported with rhai script to determine response file due to request condition. However, static, file-based or rule-based responses are expected to fulfill most cases.

Configuration

Q: Can I switch server port from the default ?
A: Yes. Two ways: run with -p | --port argument followed by specific port number. Alternatively, define it in [listener] section in apimock.toml, the root configuration. (See Configuration overview.)

Architecture

Q: How are rules loaded ?
A: At server startup.

Q: How are response files loaded ?
A: At each response (via non-blocking file I/O).

Wrapping up

We truly hope this guide helps you get the most out of our mock server and assists you in your work.

You might find other useful examples in our GitHub repository's examples directory.

If you find this project useful, a star on GitHub is a simple yet powerful way to show your support and helps our community grow.

For questions, bug reports, or feature requests, please open an issue on our GitHub Issues page. We value well-structured feedback and contributions that help improve the project for everyone in the community.

Advanced Topics

Welcome to the advanced guide for experienced users ! Here, we'll dive deeper into controlling your mock server to handle more complex and realistic scenarios. This section will equip you with the knowledge to finely tune your mocking setup and finally unlock the full power of rule-based routing.

How the server decides a response

When your mock server receives an HTTP request, it follows a specific flow to determine which response to return. Understanding this hierarchy is key to structuring your rules effectively:

Response decision flow

graph TD
    A[HTTP request received] --> B("Middleware(s) configured ?");
    B -- Yes --> C["Middleware(s) execution"];
    B -- No --> E[Rule-based routing];
    C --> D(Response returned ?);
    D -- Yes --> S[Send response];
    D -- No --> E;
    E --> F(Matched ?);
    F -- Yes --> S;
    F -- No --> G[File-based routing];
    G --> H(File found ?);
    H -- Yes --> S;
    H -- No --> I[Send 404 Not Found];

Steps

  • Middleware(s) execution:
    If middlewares are configured, the server executes them first. If a middleware explicitly returns a response, that response is sent immediately.

  • Rule-based routing:
    If no middleware responds, the server checks your rule sets from top to bottom. The first rule that completely matches the request's conditions will have its response returned.

  • File-based routing:
    If no rule matches, the server falls back to file-based routing. It tries to find a file matching the request URL path (e.g., by adding .json, .json5 or .csv extensions). If found, its content is returned, and Content-Type is automatically determined.

  • 404 Not Found:
    If no response is determined by any of the above methods, the server returns an HTTP status code 404 Not Found.

First-match strategy

Throughout this entire flow, the first-match strategy applies. This means the server will always adopt the very first valid response it finds, from middleware to file-based routing, and stop processing further.

Rule Set Configuration File Structure

Your apimock-rule-set.toml file is structured into two main parts: prefix and rules.

  • The [prefix] table allows you to define global behaviors or conditions that apply to all rules within that specific rule set file.

  • The [[rules]] array is where you define your individual mock rules. Each [[rules]] block represents one rule.

prefix table

prefix.url_path

This defines a URL path prefix for all rules in this file. Only incoming requests whose URL paths start with this defined prefix will be considered for matching by the rules in this apimock-rule-set.toml file. This is useful for organizing rules for specific API versions or modules.

  • Example: If prefix.url_path = "/api/v2", then a rule with url_path = "/users" would effectively match /api/v2/users.

prefix.respond_dir

When defining a response using respond.file_path, this prefix is prepended to your specified file path. This helps you organize all your response files in a single, common directory without having to repeat the full path in every rule.

  • Example: If prefix.respond_dir = "responses/" and a rule has respond.file_path = "user_data.json", the server will look for responses/user_data.json.

Example

# apimock-rule-set.toml
[prefix]
url_path = "/api/v2"
respond_dir = "responses/"

Rules array of tables

[[rules]], each rule record, consists of a when table and a respond table.

classDiagram
    direction LR
    class RuleSet {
        +Table prefix
        +Array~Rule~ rules
    }
    class Prefix {
        +String url_path
        +String respond_dir
    }
    class Rule {
        +Table when
        +Table respond
    }
    class When {
        +String request.url_path
        +Table request.headers
        +Table request.body.json
    }
    class Respond {
        +String file_path
        +String text
        +Integer status
        +String csv_records_key
        +Integer delay_response_milliseconds
    }

    RuleSet --|> Prefix : contains 1
    RuleSet --o Rule : contains many
    Rule --|> When : contains 1
    Rule --|> Respond : contains 1

    note for When "All conditions are ANDed."
    note for Respond "Only one of file_path or text can be used."

Here's an overview of the rule data structure in a nested Markdown format:

  • apimock-rule-set.toml
    • [prefix] (Table): Global settings for the rule set file.
      • url_path: A URL path prefix applied to all rules in this file.
      • respond_dir: A directory prefix for response file paths in this file.
    • [[rules]] (Array of Tables): Defines individual mock rules.
      Each [[rules]] entry represents a single rule and contains:
      • when (Table): Defines the matching conditions for the request.
        • request.url_path: Matches the request's URL path.
        • request.headers: Matches specific HTTP headers (case-insensitive).
        • request.body.json: Matches values within the JSON request body using dot-notation.
        • Note: All when conditions within a rule are evaluated with AND logic.
      • respond (Table): Specifies the response to be returned if the rule matches.
        • file_path: Returns content from a file (mutually exclusive with body).
        • text: Returns a string as the response body (mutually exclusive with file_path).
        • status: Sets the HTTP status code.
        • csv_records_key: Replace csv list key which is records by default.
        • delay_response_milliseconds: Mimic network delay.

Besides, the overall configuration overview is here.

when data

Each [[rules]] entry contains a when table (e.g., when.request.url_path, when.request.headers, when.request.body.json). This table defines the conditions that an incoming HTTP request must meet for this rule to be triggered.

when.request.url_path

Matches the request's URL path.

# apimock-rule-set.toml
[[rules]]
when.request.url_path = "/greetings"

when.request.headers

Matches specific HTTP headers. You can specify multiple header keys.

  • Note: According to RFC 9110, HTTP header field names are case-insensitive. Your mock server will handle this automatically.
# apimock-rule-set.toml
[[rules]]
when.request.headers.user = { value = "user1" }

when.request.body.json

Matches content within the request body. Currently, this supports matching specific keys and values within JSON request bodies.

  • JSON Body Matching: You define the target key using dot-notation paths (e.g., request.body.json.order.items.0.product_id). For array fields, use the 0-based index number (e.g., .0).
# apimock-rule-set.toml
[[rules]]
when.request.body.json.order.items.0.product_id = { value = "123" }

Multiple conditions strategy

Important: If you define multiple conditions (e.g., a path, a header, and a body match) within a single [[rules]] block, they are all evaluated using AND logic. All conditions must be met for the rule to match the incoming request.

respond data

The respond table within each [[rules]] block specifies the response to be returned when the rule's when conditions are fully met. You must define one of the following.

respond.file_path

Returns the content of a specified file. The Content-Type header is automatically determined from the file's extension (e.g., .json will set application/json).

# apimock-rule-set.toml
[[rules]]
# when ...
respond.file_path = "response.json"

respond.text

Returns the specified string as the response body. The Content-Type header is text/plain.

# apimock-rule-set.toml
[[rules]]
# when ...
respond.text = "My reply !"

respond.status

Sets the HTTP status code for the response (e.g., 200 for OK, 404 for Not Found).

# apimock-rule-set.toml
[[rules]]
# when ...
respond.status = 401

Limitation

You cannot specify both respond.file_path and respond.text in the same rule.

Configuration overview

classDiagram
    direction LR
    class Config {
        +Table listener
        +Table log.verbose
        +Table service
    }
    class ListenerConfig {
        +String ip_address
        +Integer port
    }
    class LogConfig.VerboseConfig {
        +Boolean header
        +Boolean body
    }
    class ServiceConfig {
        +Array~RuleSet~ rule_sets
        +Array~RuleSet~ middlewares
        +String fallback_respond_dir
    }

    Config --|> ListenerConfig : contains 1
    Config --|> LogConfig.VerboseConfig : contains 1
    Config --|> ServiceConfig : contains 1

Here's an overview of the rule data structure in a nested Markdown format:

  • apimock.toml
    • [listener] (Table): Server listener.
      • ip_address
      • port
    • [log] (Table): Logger.
      • verbose.header: Verbose on request header.
      • verbose.body: Verbose on request body.
    • [service] (Table): App service
      • rule_sets: Rule-based routing. The detail is here.
      • middlewares
      • fallback_respond_dir: File-based routing base. The default is ., your current directory.

Middleware with Rhai scripts

While your mock server supports middlewares written using Rhai scripts for highly dynamic scenarios, our goal is to minimize the need for custom script creation and maintenance. We believe that file-based and rule-based matching definitions can cover almost all practical needs you'll encounter.

Therefore, we generally do not recommend using middlewares unless you have a very specific and complex requirement that cannot be met by combining rules and operators.

However, for those unique cases, Rhai is a powerful, embedded scripting language that feels very similar to JavaScript or Rust.

Here are some basic Rhai code examples you might use.

Variable Definition

let my_variable = "Hello, Rhai !";
let count = 10;

If / Else Statements

if url_path == "/dynamic_response" {
    return "response_for_dynamic.json";\

// else clause is also available:
// } else {
//    return "default_response.json";

}

Switch (Match) Statements

switch (url_path) {
    "/middleware-test/dummy" if body.middleware == "isHere" => {
        // exit() is useful when run in fn (here, equivalent to return statement):
        exit(returned_json_file_path);
    },
    _ => ()
}

Return Statement

Middleware scripts primarily return a file path string. If a middleware returns a value, the server will use it as the response.

#![allow(unused)]
fn main() {
return "path/to/response.json";
}

To learn more about Rhai's syntax and capabilities

you can refer to the official Rhai documentation. Use this feature judiciously and only when other options fall short.

Technical referrence: A Guide for Developers and Contributors

This document serves as the comprehensive technical reference for our app, designed for developers and contributors. It provides an in-depth understanding of the project's foundational elements.

Herein, you will find detailed information regarding:

  • Project Vision and Goals: An outline of the core problems our app aims to solve, its guiding principles.
  • Architectural Overview: The necessary context for comprehending the broader system design.
  • Detailed Design Specifications: A granular exploration of key design decisions, data structures, algorithms.

Note: This technical reference is currently less mature than our user guide or advanced topics documentation. Contributions to its further development and refinement are always appreciated and considered.

Vision and Goals

Vision

Aims to be a developer-friendly, robust and functional HTTP mock server which doesn't require complicated configuration and accepts rich customization around routing.

Designed in mind with

  • Easy setup / usage
    • Built as single (and small) executable, integrated configuration. (No need to write scripts, config-less mode is also supported.)
  • Performance
    • Fast speed, low memory consumption.
  • Cross-platform support

Goals

1. Basic

  • File-based routing frees us around hard setup
  • Supports .json / .json5 / .csv files treated as JSON Response

2. Customization

  • Rule-based routing empowers us
  • Can specify response time on all or each API path
  • Custom HTTP response codes: 3xx as redirects, and 4xx and 5xx as errors

3. Dynamic processing

  • Flexible responses with condition combination. Even with the same API URL path, multiple responses can be returned.
  • Optionally, middleware as Rhai scripts is available to customize request routing and response handling

4. Safe and observable usage

  • Validates configuration: Missing JSON files, duplicate paths etc.
  • Prints out routing at startup
  • Describes request content on both HTTP headers and body (json or plain text) when verbose on log config is enabled
  • Integrated test cases for app stability and robustness

5. GUI wrapper integration

  • Achieved via cargo feature: When spawn is activated, the server is available as subprocess. The output will be returned via tokio mpsc queue.

Architecture

Core Components

  • based on hyper v1

  • src/config.rs
    Defines configuration structures for customizable parameters like the server port, root path, logging levels, etc.

  • src/server.rs
    The HTTP server entry point powered by hyper. It handles incoming requests and delegates them to the core logic.

  • src/core/server/routing.rs
    Contains core logic including request routing, matching rules, response rendering, and script evaluation.

  • tests/
    Includes test cases run by cargo test.

Design

To facilitate a deeper understanding of the mock server's capabilities and internal workings, this section outlines its core architectural design. It serves as a foundational reference for developers seeking to grasp the system's operational mechanics or contemplating contributions. We will detail the sequential steps involved in server initialization, followed by a comprehensive exploration of the request-to-response lifecycle, each supported by illustrative process diagrams.

Server launch

Workflow

flowchart TD
    A[get env args]
    B["app new()"]
    C["init_logger()"]
    
    D(config file path env arg overwriting?)
    E[replace config or default values with env arg]
    
    F[load config]
    G[validate]
    H[dump config]
    
    I(port env arg overwriting?)
    J[replace config or default values with env arg]
    
    K[start to listen]

    A --> B --> C --> D
    D --yes--> E --> F
    D --no--> F
    F --> G --> H --> I
    I --yes--> J --> K
    I --no--> K

load config

flowchart TD
    subgraph LoadConfig
        LC0[load root config]
        LC1[load middlewares]
        LC2[load rule sets]
    
        subgraph LC3[for each rule set]
            subgraph LC4[for each rule]
                LC5[compute derived fields]
            end
        end
    end

    Note1["note: if any file offered as file path is missing, process will abort"]
    style Note1 stroke:none

    LC0 --> LC1 --> LC2 -.- LC3
    LC3 --> LC4
    LC4 --> LC5
    LoadConfig -.- Note1

validate

flowchart TD
    subgraph Validate
        A1[root config validate]
        subgraph For_Rule_Sets
            A2[validate rule set]
            subgraph For_Rules
                A3[validate rule]
                subgraph For_Whens_Responds
                    A4[validate]
                end
            end
        end
    end

    A1 --> For_Rule_Sets
    A2 --> For_Rules
    A3 --> For_Whens_Responds

dump config

flowchart TD
    subgraph Dump_Config
        A1[dump root config]
        subgraph For_Each_Rule_Sets
            A2[dump rule set]
            subgraph For_Each_Rules
                A3[dump rule]
                subgraph For_Whens_Responds
                    A4[dump their children and themselves]
                end
            end
        end
    end

    A1 --> For_Each_Rule_Sets
    A2 --> For_Each_Rules
    A3 --> For_Whens_Responds

Response

Workflow

flowchart TD

    A[request received]
    OP1(OPTIONS method ?)
    OP2[200 OK]
    OP3((end))
    B[logger dumps request]
    C(middlewares enabled?)
    MW[[middleware]]
    MWC("completed ?")
    MWCZ((end))
    D(rule sets enabled?)
    RR[[rule-based routing]]
    RRC("completed ?")
    RRCZ((end))
    FR[[file-based routing]]
    FRC("completed ?")
    FRCZ((end))
    Z2[not found response]
    Z2Z((end))

    A --> OP1
    OP1 -->|Yes| OP2
    OP1 -->|No| B
    OP2 --> OP3
    B --> C
    C -->|Yes| MW
    C -->|No| D
    MW --> MWC
    MWC -->|Yes| MWCZ
    MWC -->|No| D
    D --> |Yes| RR
    D --> |No| FR
    RR --> RRC
    RRC -->|Yes| RRCZ
    RRC -->|No| FR
    FR --> FRC
    FRC -->|Yes| FRCZ
    FRC -->|No| Z2
    Z2 --> Z2Z

Middleware

flowchart TD

    subgraph MW[for each middleware]
        MW1[run rhai]
        MW2(file path returned ?)

        subgraph MWR[return response]
            MWR1(content got ?)
            MWR2[return response]
            MWR3[error response]
        end

        Z(("continue"))
    end

    MW1 -->MW2
    MW2 -->|Yes| MWR
    MW2 -->|No| Z
    MWR1 -->|Yes| MWR2
    MWR1 -->|No| MWR3

Rule-based routing

flowchart TD

    subgraph RS[for each rule set]
      subgraph RR[for each rule]
          RR1["is_match()"]
          RR2(matched entry found ?)
          RR2Z((continue))

          subgraph RRR[return response]
              RRR1(file_path or text ?)
              RRR2(content got ?)
              RRR3[return response]
              RRR4[error response]
          end
      end
    end

    RR1 -->RR2
    RR2 -->|Yes| RRR
    RR2 -->|No| RR2Z
    RRR1 -->|file_path| RRR2
    RRR1 -->|text| RRR3
    RRR2 -->|Yes| RRR3
    RRR2 -->|No| RRR4

File-based routing

  • try to use request url_path as file path from fallback_respond_dir
  • try to find file as it is and then ones which are attached supported extensions to
flowchart TD

    subgraph FSE["for each raw path or extension-attached"]
        FSE1(file found ?)
        FSE1Z((continue))

        subgraph FSER[return response]
            FSER1(file_path or text ?)
            FSER2(content got ?)
            FSER3[return response]
            FSER4[error response]
        end
    end

    FSE1 -->|Yes| FSER
    FSE1 -->|No| FSE1Z
    FSER1 -->|file_path| FSER2
    FSER1 -->|text| FSER3
    FSER2 -->|Yes| FSER3
    FSER2 -->|No| FSER4

Response headers

The proper configuration of HTTP response headers is paramount for any API. Even in the context of mock servers, these seemingly minor details significantly influence the stability of communication between the browser (client) and the server, serve as the primary mechanism for CORS (Cross-Origin Resource Sharing) compliance, and ultimately contribute directly to the productivity of API developers

Default response headers

For connection stability

HeaderValue
DateResponse date
Content-LengthCalculated from content body
Connectionkeep-alive
Cache-Controlno-store

For security

HeaderValue
x-content-type-optionsnosniff

CORS (Cross-Origin Resource Sharing)

Default CORS variables in response

HeaderValue
Access-Control-Allow-MethodsGET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers*
Access-Control-Max-Age86400

Variation due to request

If Origin is got from request and looks like authorized request including Cookie or Authorization header:

HeaderValue
Access-Control-Allow-OriginRequest Origin value
VaryOrigin
Access-Control-Allow-Credentialstrue

Else:

HeaderValue
Access-Control-Allow-Origin*
Vary*
Access-Control-Allow-Credentials(not set)

Response to OPTIONS request

It is crucial for the server to immediately respond to OPTIONS method requests, unlike other HTTP methods. This is because OPTIONS requests trigger what is known as a "preflight request" in CORS.

The purpose of this preflight OPTIONS request is to ask the server for permission before sending the actual request. The browser needs to know if the server understands the method, headers, and credentials that will be used in the actual request.

The server must be configured to process OPTIONS requests as preflight checks, responding with the appropriate CORS headers without attempting to process them as regular API calls. This ensures that the browser receives the necessary permissions to proceed with the actual cross-origin requests, thereby stabilizing browser-server communication according to HTTP and CORS specifications.

Response headers to OPTIONS request

The headers include the server's default Access-Control-Allow-Methods, but differ in the key aspects below:

  • HTTP Status Code: 204 No Content
  • Content-Length: 0