> For the complete documentation index, see [llms.txt](https://docs.digitalapi.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.digitalapi.ai/api-gateway/plugin-reference/plugins-general.md).

# General plugins

Plugins in this category provide utility behaviour that does not fit a more specific category: redirects, mirroring, request validation, response substitution. Useful when the policy you need does not map cleanly to authentication, traffic control, or observability. The catalogue ships 12 General plugins.

{% hint style="info" %}
**In this category:** [`echo`](#echo), [`ext-plugin-post-req`](#ext-plugin-post-req), [`ext-plugin-post-resp`](#ext-plugin-post-resp), [`ext-plugin-pre-req`](#ext-plugin-pre-req), [`gzip`](#gzip), [`inspect`](#inspect), [`key-value-map`](#key-value-map), [`real-ip`](#real-ip), [`redirect`](#redirect), [`request-chain`](#request-chain), [`request-echo`](#request-echo), [`service-callout`](#service-callout)
{% endhint %}

### `echo`

The `echo` Plugin is to help users understand how they can develop a Helix Plugin.

{% hint style="info" %}
**Priority** `412` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

This Plugin addresses common functionalities in phases like init, rewrite, access, balancer, header filter, body filter and log.

:::caution WARNING

The `echo` Plugin is built as an example. It has missing cases and should **not** be used in production environments.

:::

| Name         | Type   | Requirement | Default | Valid | Description                               |
| ------------ | ------ | ----------- | ------- | ----- | ----------------------------------------- |
| before\_body | string | optional    |         |       | Body to use before the filter phase.      |
| body         | string | optional    |         |       | Body that replaces the Upstream response. |
| after\_body  | string | optional    |         |       | Body to use after the modification phase. |
| headers      | object | optional    |         |       | New headers to use for the response.      |

**Sample configuration**

Replace the upstream response body with a fixed string:

```json
{
  "echo": {
    "body": "Hello from Helix"
  }
}
```

Wrap the upstream response with content before and after it:

```json
{
  "echo": {
    "before_body": "<prefix>",
    "after_body": "</suffix>"
  }
}
```

Add response headers alongside a replacement body:

```json
{
  "echo": {
    "body": "Hello from Helix",
    "headers": {
      "X-Served-By": "helix-echo"
    }
  }
}
```

</details>

### `ext-plugin-post-req`

`ext-plugin-post-req` differs from the ext-plugin-pre-req Plugin in that it runs after executing the built-in Lua Plugins and before proxying to the Upstream.

{% hint style="info" %}
**Priority** `-3000` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

You can learn more about the configuration from the ext-plugin-pre-req Plugin document.

**Sample configuration**

Run a single external Plugin on the Plugin Runner after the built-in Plugins:

```json
{
  "ext-plugin-post-req": {
    "conf": [
      { "name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}" }
    ]
  }
}
```

Allow requests to continue when the Plugin Runner is unavailable:

```json
{
  "ext-plugin-post-req": {
    "conf": [
      { "name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}" }
    ],
    "allow_degradation": true
  }
}
```

</details>

### `ext-plugin-post-resp`

The `ext-plugin-post-resp` Plugin is for running specific external Plugins in the Plugin Runner before executing the built-in Lua Plugins.

{% hint style="info" %}
**Priority** `-4000` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

The `ext-plugin-post-resp` plugin will be executed after the request gets a response from the upstream.

This plugin makes its own outbound HTTP call to the upstream, due to which the proxy-control, proxy-mirror, and proxy-cache plugins are not available to be used alongside this plugin. Also, mTLS Between Helix and Upstream is not yet supported.

See External Plugin to learn more.

:::note

Execution of External Plugins will affect the response of the current request.

:::

| Name               | Type    | Required | Default | Valid values                                                 | Description                                                                                                            |
| ------------------ | ------- | -------- | ------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| conf               | array   | False    |         | \[{"name": "ext-plugin-A", "value": "{"enable":"feature"}"}] | List of Plugins and their configurations to be executed on the Plugin Runner.                                          |
| allow\_degradation | boolean | False    | false   |                                                              | Sets Plugin degradation when the Plugin Runner is not available. When set to `true`, requests are allowed to continue. |

**Sample configuration**

Run an external Plugin on the response after the upstream replies:

```json
{
  "ext-plugin-post-resp": {
    "conf": [
      { "name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}" }
    ]
  }
}
```

Continue serving the response when the Plugin Runner is unavailable:

```json
{
  "ext-plugin-post-resp": {
    "conf": [
      { "name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}" }
    ],
    "allow_degradation": true
  }
}
```

</details>

### `ext-plugin-pre-req`

The `ext-plugin-pre-req` Plugin is for running specific external Plugins in the Plugin Runner before executing the built-in Lua Plugins.

{% hint style="info" %}
**Priority** `12000` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

See External Plugin to learn more.

:::note

Execution of External Plugins will affect the behavior of the current request.

:::

| Name               | Type    | Required | Default | Valid values                                                 | Description                                                                                                            |
| ------------------ | ------- | -------- | ------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| conf               | array   | False    |         | \[{"name": "ext-plugin-A", "value": "{"enable":"feature"}"}] | List of Plugins and their configurations to be executed on the Plugin Runner.                                          |
| allow\_degradation | boolean | False    | false   |                                                              | Sets Plugin degradation when the Plugin Runner is not available. When set to `true`, requests are allowed to continue. |

**Sample configuration**

Run a single external Plugin on the Plugin Runner before the built-in Plugins:

```json
{
  "ext-plugin-pre-req": {
    "conf": [
      { "name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}" }
    ]
  }
}
```

Allow requests to continue when the Plugin Runner is unavailable:

```json
{
  "ext-plugin-pre-req": {
    "conf": [
      { "name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}" }
    ],
    "allow_degradation": true
  }
}
```

</details>

### `gzip`

The `gzip` Plugin dynamically sets gzip response compression at the gateway. When the `gzip` plugin is enabled, the client needs to include `Accept-Encoding: gzip` in the request header to indicate support for gzip compression. Upon receiving the request, Helix dynamically determines whether to compress the response content based on the client's support and server configuration. If the conditions are met, `Helix` adds the `Content-Encoding: gzip` header to the response, indicating that the response content has been compressed using gzip. Upon receiving the response, the client uses the corresponding decompression algorithm based on the `Content-Encoding` header to decompress the response content and obtain the original response content.

{% hint style="info" %}
**Priority** `995` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

:::info IMPORTANT

This Plugin requires Helix to run on Helix-Runtime.

:::

| Name           | Type                   | Required | Default        | Valid values | Description                                                                             |
| -------------- | ---------------------- | -------- | -------------- | ------------ | --------------------------------------------------------------------------------------- |
| types          | array\[string] or "\*" | False    | \["text/html"] |              | Dynamically sets the `gzip_types` directive. Special value `"*"` matches any MIME type. |
| min\_length    | integer                | False    | 20             | >= 1         | Dynamically sets the `gzip_min_length` directive.                                       |
| comp\_level    | integer                | False    | 1              | \[1, 9]      | Dynamically sets the `gzip_comp_level` directive.                                       |
| http\_version  | number                 | False    | 1.1            | 1.1, 1.0     | Dynamically sets the `gzip_http_version` directive.                                     |
| buffers.number | integer                | False    | 32             | >= 1         | Dynamically sets the `gzip_buffers` directive parameter `number`.                       |
| buffers.size   | integer                | False    | 4096           | >= 1         | Dynamically sets the `gzip_buffers` directive parameter `size`. The unit is in bytes.   |
| vary           | boolean                | False    | false          |              | Dynamically sets the `gzip_vary` directive.                                             |

**Sample configuration**

Compress common text and JSON response types:

```json
{
  "gzip": {
    "types": ["text/html", "text/plain", "application/json"]
  }
}
```

Only compress responses above 1 KB and raise the compression level:

```json
{
  "gzip": {
    "types": ["text/html", "application/json"],
    "min_length": 1024,
    "comp_level": 6,
    "vary": true
  }
}
```

Compress any MIME type and tune the buffers:

```json
{
  "gzip": {
    "types": "*",
    "buffers": {
      "number": 16,
      "size": 8192
    }
  }
}
```

</details>

### `inspect`

It's useful to set arbitrary breakpoint in any Lua file to inspect the context information, e.g. print local variables if some condition satisfied.

{% hint style="info" %}
**Priority** `200` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

In this way, you don't need to modify the source code of your project, and just get diagnose information on demand, i.e. dynamic logging.

This plugin supports setting breakpoints within both interpretd function and jit compiled function. The breakpoint could be at any position within the function. The function could be global/local/module/ananymous.

| Name        | Type    | Required | Default                                       | Description                                                                                                                                 |
| ----------- | ------- | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| delay       | integer | False    | 3                                             | Time in seconds specifying how often to check the hooks file.                                                                               |
| hooks\_file | string  | False    | "/usr/local/helix/plugin\_inspect\_hooks.lua" | Lua file to define hooks, which could be a link file. Ensure only administrator could write this file, otherwise it may be a security risk. |

**Sample configuration**

Enable inspection with the default hooks file and check interval:

```json
{
  "inspect": {}
}
```

Check the hooks file more frequently and point at a custom path:

```json
{
  "inspect": {
    "delay": 1,
    "hooks_file": "/usr/local/helix/my_hooks.lua"
  }
}
```

</details>

### `key-value-map`

The `key-value-map` plugin securely fetches encrypted key-value pairs from the gateway's key-value store, decrypts them using vault-managed keys, and exposes them in the gateway context (`ctx.helix.key_value_map`) for downstream plugins or upstreams.

{% hint style="info" %}
**Priority** `3001` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

| Name           | Type           | Required | Default         | Valid values      | Description                                                                                                                                                                                                                                                                                                                            |
| -------------- | -------------- | -------- | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| fetch.keys     | array          | True     |                 | 1 to 128 entries  | Keys to fetch. Each entry is either a key name (string) or an object with `key`, an optional `alias` under which the value is exposed, and an optional `map-identifier` to read the key from a named map.                                                                                                                              |
| inserts        | array\[object] | False    |                 | up to 64 entries  | Key-value pairs to write. Each entry has `key`, `value`, an optional `ttl` in seconds (minimum 1), an optional `map-identifier` to scope the entry to a named map, and an optional `encrypt` (boolean). When `encrypt` is `true`, that value is stored encrypted at rest (the encryption feature must be enabled for your deployment). |
| fail\_action   | string         | False    | `open`          | \["open","close"] | `open` continues the request when a fetch fails; `close` rejects it.                                                                                                                                                                                                                                                                   |
| ctx\_namespace | string         | False    | `key_value_map` |                   | Namespace under `ctx.helix` where fetched key-value pairs are stored.                                                                                                                                                                                                                                                                  |

**Reference variables**

Inside `inserts`, a `key` or `value` may contain **reference variables** resolved per request from the request context: `$ctx.<field>`, `$request.<field>`, `$response.<field>`, and the authenticated caller's identity `$consumer.<field>` and `$user.<field>`. A `$consumer.<suffix>` reference resolves to `<consumer name>.<suffix>`, giving a per-consumer naming convention that a later fetch and downstream plugins can resolve the same way. For example, storing a caller's key under `$consumer.private_key` lets [`pgp-crypto`](/api-gateway/plugin-reference/plugins-transformation.md#pgp-crypto) look it up as a dynamic key reference for that same caller. Per-consumer key material (`.private_key`, `.passphrase`) is masked in logs.

A `$response.body.*` reference resolves from the captured response body. On responses larger than about 1 MiB the body is captured only up to that cap, so a `$response.body.*` reference beyond it may not resolve; a note is logged when the cap is reached.

**Sample configuration**

Fetch a set of keys, including one exposed under an alias:

```json
{
  "key-value-map": {
    "fetch": {
      "keys": [
        "billing_account",
        { "key": "partner_api_key", "alias": "partner_key" }
      ]
    }
  }
}
```

Reject the request when a fetch fails instead of continuing:

```json
{
  "key-value-map": {
    "fetch": {
      "keys": ["billing_account"]
    },
    "fail_action": "close"
  }
}
```

Write key-value pairs into the store, one with a time-to-live:

```json
{
  "key-value-map": {
    "inserts": [
      { "key": "feature_flag", "value": "enabled" },
      { "key": "session_token", "value": "abc123", "ttl": 3600 }
    ]
  }
}
```

</details>

### `real-ip`

The `real-ip` Plugin allows Helix to set the client's real IP by the IP address passed in the HTTP header or HTTP query string. This is particularly useful when Helix is behind a reverse proxy since the proxy could act as the request-originating client otherwise.

{% hint style="info" %}
**Priority** `23000` · **Category** General · **Required** `source`
{% endhint %}

<details>

<summary>Configuration and details</summary>

The Plugin is similar to a standard reverse-proxy real-IP capability but offers more flexibility.

| Name               | Type           | Required | Default | Valid values                                               | Description                                                                                                                                                                                                                                                                                                                          |
| ------------------ | -------------- | -------- | ------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| source             | string         | True     |         |                                                            | A built-in Helix variable or gateway variable, such as `http_x_forwarded_for` or `arg_realip`. The variable value should be a valid IP address that represents the client's real IP address, with an optional port.                                                                                                                  |
| trusted\_addresses | array\[string] | False    |         | array of IPv4 or IPv6 addresses (CIDR notation acceptable) | Trusted addresses that are known to send correct replacement addresses. This configuration sets the `set_real_ip_from` directive.                                                                                                                                                                                                    |
| recursive          | boolean        | False    | False   |                                                            | <p>If false, replace the original client address that matches one of the trusted addresses by the last address sent in the configured <code>source</code>.<br>If true, replace the original client address that matches one of the trusted addresses by the last non-trusted address sent in the configured <code>source</code>.</p> |

**Sample configuration**

Take the client IP from the `X-Forwarded-For` header:

```json
{
  "real-ip": {
    "source": "http_x_forwarded_for"
  }
}
```

Take the client IP from a query argument instead of a header:

```json
{
  "real-ip": {
    "source": "arg_realip"
  }
}
```

Trust known proxy ranges and resolve recursively to the real client:

```json
{
  "real-ip": {
    "source": "http_x_forwarded_for",
    "trusted_addresses": ["10.0.0.0/8", "192.168.0.0/16"],
    "recursive": true
  }
}
```

</details>

### `redirect`

The `redirect` Plugin can be used to configure redirects.

{% hint style="info" %}
**Priority** `900` · **Category** General
{% endhint %}

<details>

<summary>Configuration and details</summary>

| Name                  | Type           | Required | Default | Valid values | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------- | -------------- | -------- | ------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| http\_to\_https       | boolean        | False    | false   |              | When set to `true` and the request is HTTP, it will be redirected to HTTPS with the same URI with a 301 status code. Note the querystring from the raw URI will also be contained in the Location header.                                                                                                                                                                                                                                                         |
| uri                   | string         | False    |         |              | URI to redirect to. Can contain gateway variables. For example, `/test/index.html`, `$uri/index.html`, `${uri}/index.html`, `https://example.com/foo/bar`. If you refer to a variable name that doesn't exist, instead of throwing an error, it will treat it as an empty variable.                                                                                                                                                                               |
| regex\_uri            | array\[string] | False    |         |              | Match the URL from client with a regular expression and redirect. If it doesn't match, the request will be forwarded to the Upstream. Only either of `uri` or `regex_uri` can be used at a time. For example, \[" ^/app/(.*)/(.*)/(.\*)", "/$1-$2-$3"], where the first element is the regular expression to match and the second element is the URI to redirect to. Helix only support one `regex_uri` currently, so the length of the `regex_uri` array is `2`. |
| ret\_code             | integer        | False    | 302     | \[200, ...]  | HTTP response code.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| encode\_uri           | boolean        | False    | false   |              | When set to `true` the URI in the `Location` header will be encoded as per [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986).                                                                                                                                                                                                                                                                                                                              |
| append\_query\_string | boolean        | False    | false   |              | When set to `true`, adds the query string from the original request to the `Location` header. If the configured `uri` or `regex_uri` already contains a query string, the query string from the request will be appended to it with an `&`. Do not use this if you have already handled the query string (for example, with an gateway variable `$request_uri`) to avoid duplicates.                                                                              |

**Sample configuration**

Redirect every request to a fixed URI:

```json
{
  "redirect": {
    "uri": "/test/index.html"
  }
}
```

Rewrite the path with a regular expression and return a 301:

```json
{
  "redirect": {
    "regex_uri": ["^/app/(.*)/(.*)/(.*)", "/$1-$2-$3"],
    "ret_code": 301
  }
}
```

Redirect HTTP requests to HTTPS, keeping the original query string:

```json
{
  "redirect": {
    "http_to_https": true,
    "append_query_string": true
  }
}
```

</details>

### `request-chain`

The `request-chain` Plugin runs an ordered list of steps across the **rewrite** and **access** phases. Each step invokes another plugin with its own configuration, letting you compose behaviours such as a callout followed by an enforcement check inside a single attachment.

{% hint style="info" %}
**Priority** `1005` · **Category** General · **Required** `steps`
{% endhint %}

{% hint style="danger" %}
**Caution:** The chain editor offers only the plugins that fit as a step, and the API editor refuses a chain that names one that does not. A `request-chain` created in an earlier release can still hold a step whose plugin does no work in the rewrite or access phases: it stays stored and runs half-working, with nothing reported. After upgrading, re-open older `request-chain` attachments and confirm each step is one the editor still offers.
{% endhint %}

<details>

<summary>Configuration and details</summary>

**How steps run.** A step's phase is not something you set: it is taken from the step plugin itself. A plugin that does its work in the **rewrite** phase runs in rewrite, one that works in the **access** phase runs in access, and one that works in both runs in both. **Every rewrite step runs before every access step**, and within a phase steps run in the order you list them, never by plugin priority. If a step's plugin does nothing in the current phase, it is skipped for that phase rather than treated as an error. `lua-callout` and the serverless step plugins register for every phase and stay idle unless their own `phase` matches, so you steer them with a `"phase": "rewrite"` or `"phase": "access"` inside the step's `conf`.

**Which plugins fit as a step.** The chain editor lists only the plugins that fit: those that act in the rewrite or access phase and do no work in the later response or logging phases. This includes the authentication plugins that run in rewrite (for example `key-auth`, `jwt-auth`, `basic-auth`, `openid-connect`) and transformation plugins such as `proxy-rewrite` and `redirect`. A plugin whose real work is in a later phase (for example `helix-analytics`, `limit-conn`, `api-breaker`, or any logger) is not offered, because that work would never run inside a chain; attach those on the route instead. `key-value-map` and `lua-callout` always fit, and `request-chain` cannot be a step of itself.

| Name           | Type           | Required | Default         | Valid values      | Description                                                                                                                                                                                        |
| -------------- | -------------- | -------- | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| steps          | array\[object] | True     |                 | 1 to 50 entries   | Ordered steps. Each step names a `plugin` that fits as a step and supplies its `conf`. A step's `_meta.filter` vars expression can skip that step at runtime.                                      |
| error\_policy  | string         | False    | `close`         | \["open","close"] | `close` stops the chain on a step failure, a missing step plugin, or a step HTTP error (status >= 400); the body uses `_meta.error_response` when set. `open` logs and continues to the next step. |
| error\_status  | integer        | False    | 503             | \[400, ..., 599]  | HTTP status for synthesized errors (step failure, or a missing step plugin at runtime) when `error_policy` is `close`.                                                                             |
| ctx\_namespace | string         | False    | `request_chain` |                   | Namespace under `ctx.helix` where a step failure records `failed` and its `fail_reason`.                                                                                                           |

**Sample configuration**

Place each step in a phase by choosing step plugins that act there. The rewrite step runs first even though it is listed second:

```json
{
  "request-chain": {
    "steps": [
      {
        "plugin": "lua-callout",
        "conf": { "phase": "access", "functions": ["..."] }
      },
      {
        "plugin": "lua-callout",
        "conf": { "phase": "rewrite", "functions": ["..."] }
      }
    ]
  }
}
```

Run a callout followed by an enforcement check as an ordered chain:

```json
{
  "request-chain": {
    "steps": [
      {
        "plugin": "service-callout",
        "conf": {
          "uri": "https://auth.internal/verify",
          "phase": "access"
        }
      },
      {
        "plugin": "api-product-enforcer",
        "conf": {}
      }
    ]
  }
}
```

Continue past a failing step instead of stopping the chain:

```json
{
  "request-chain": {
    "steps": [
      {
        "plugin": "key-value-map",
        "conf": {
          "fetch": { "keys": ["partner_api_key"] }
        }
      }
    ],
    "error_policy": "open",
    "error_status": 502
  }
}
```

</details>

### `request-echo`

The `request-echo` Plugin is a diagnostic tool. Instead of proxying the request, it answers it with a JSON document describing the request **as the gateway was about to send it upstream**: method, path, query parameters, headers, raw body, decoded JSON, form fields, and the field names of any uploaded files. It replaces the common trick of repointing a route at an external echo service just to see the final request.

{% hint style="info" %}
**Priority** `-5000` · **Category** General
{% endhint %}

{% hint style="danger" %}
**Caution:** The response contains the request headers verbatim, including credentials such as `Authorization`, API keys, and cookies. Attach this Plugin deliberately while debugging and remove it afterwards; never leave it on a production route.
{% endhint %}

<details>

<summary>Configuration and details</summary>

The Plugin takes **no configuration**: attaching it enables it, removing it disables it. There is no `enabled` flag.

| Name | Type | Required | Default | Description                                          |
| ---- | ---- | -------- | ------- | ---------------------------------------------------- |
| -    | -    | -        | -       | This Plugin has no attributes. Configure it as `{}`. |

It runs in the **`before_proxy`** phase at the lowest priority, so it runs after every other plugin on the route and reports the request exactly as it stands at that point: headers rewritten by [`proxy-rewrite`](/api-gateway/plugin-reference/plugins-transformation.md#proxy-rewrite), bodies changed by [`body-transformer`](/api-gateway/plugin-reference/plugins-transformation.md#body-transformer), and so on are all reflected. The Plugin never modifies the request, never writes to the context, and makes no external calls.

{% hint style="info" %}
**Note:** The upstream is selected before this phase runs, so a route whose upstream cannot be resolved fails with `502` / `503` before the echo is produced. Use `request-echo` to inspect a request on a route whose upstream is reachable; it cannot help debug an unreachable upstream.
{% endhint %}

**Response**

`200 OK` with `Content-Type: application/json`. The response also carries a header marking it as the gateway's own rather than an upstream's (JSON key order is not guaranteed, so the header is the reliable marker):

```
X-Echoed-By: HELIX/<version>
```

The body repeats this in `echoed_by` and wraps the request under a `request` object:

```json
{
  "success": true,
  "status": 200,
  "message": "Request received successfully",
  "echoed_by": "Helix Gateway (request-echo plugin); the upstream was not contacted",
  "request": {
    "method": "POST",
    "path": "/api/orders",
    "query": { "trace": "1" },
    "headers": { "content-type": "application/json" },
    "body": "{\"item\":\"abc\"}"
  }
}
```

A binary request body cannot be placed in JSON as-is: invalid byte sequences are replaced with the Unicode replacement character, while text that is already valid UTF-8 is never altered.

**Sample configuration**

Enable it on the route (it takes no fields):

```json
{
  "request-echo": {}
}
```

</details>

### `service-callout`

The `service-callout` Plugin performs independent outbound HTTP calls to external services via a direct URI. The response is stored in `ctx.helix.<ctx_namespace>` so that downstream plugins can consume it. The plugin supports the rewrite and access phases and can run callouts either synchronously (default) or asynchronously (fire-and-forget).

{% hint style="info" %}
**Priority** `2003` · **Category** General · **Required** `uri`, `phase`
{% endhint %}

<details>

<summary>Configuration and details</summary>

| Name                    | Type           | Required | Default                                                  | Valid values                                 | Description                                                                                                                                                                          |
| ----------------------- | -------------- | -------- | -------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| uri                     | string         | True     |                                                          |                                              | Full URI for the outbound HTTP call.                                                                                                                                                 |
| phase                   | string         | True     |                                                          | \["rewrite","access"]                        | Request phase in which to run the callout.                                                                                                                                           |
| method                  | string         | False    | `GET`                                                    | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS | HTTP method used for the outbound call.                                                                                                                                              |
| timeout                 | integer        | False    | 3000                                                     | \[1, ..., 60000]                             | Outbound request timeout in milliseconds.                                                                                                                                            |
| ssl\_verify             | boolean        | False    | true                                                     |                                              | Whether to verify the TLS certificate of the external service.                                                                                                                       |
| forwarded\_headers      | array\[string] | False    |                                                          |                                              | Client request header names to forward to the outbound call.                                                                                                                         |
| custom\_headers         | object         | False    |                                                          |                                              | Headers added to the outbound call. Values support gateway variable substitution (`$var` / `${var}`) and `$ctx.<path>` references, for example `$ctx.helix.helix_auth.access_token`. |
| custom\_query\_params   | object         | False    |                                                          |                                              | Query parameters added to the outbound call. Values support gateway variable substitution (`$var` / `${var}`) and `$ctx.<path>` references.                                          |
| send\_request\_object   | boolean        | False    | false                                                    |                                              | When true, send a JSON body containing the current request (method, URI, headers, body, query).                                                                                      |
| request\_body\_template | string         | False    |                                                          |                                              | Custom request body template with variable substitution. If set, this overrides `send_request_object`.                                                                               |
| map\_response\_to\_ctx  | object         | False    | `{"external_body": "body", "external_status": "status"}` |                                              | Map response fields into `ctx.helix.<ctx_namespace>`. Paths: `status`, `body`, `body.<field>.<nested>`, `headers`, `headers.<name>`.                                                 |
| async                   | boolean        | False    | false                                                    |                                              | When true, execute the callout asynchronously (fire-and-forget). When false, execute synchronously and store the response in ctx.                                                    |
| error\_handling.policy  | string         | False    | `fail-open`                                              | \["fail-open","fail-close","log-only"]       | `fail-open` continues and stores the error in ctx. `fail-close` returns 503. `log-only` logs and continues.                                                                          |
| error\_handling.message | string         | False    |                                                          |                                              | Custom error message returned when the policy is `fail-close`.                                                                                                                       |
| keepalive.enabled       | boolean        | False    | true                                                     |                                              | Whether to reuse connections for outbound calls.                                                                                                                                     |
| keepalive.timeout       | integer        | False    | 60000                                                    | >=1000                                       | Keepalive timeout in milliseconds.                                                                                                                                                   |
| keepalive.pool\_size    | integer        | False    | 5                                                        | >=1                                          | Maximum number of idle connections in the keepalive pool.                                                                                                                            |
| ctx\_namespace          | string         | False    | `service_callout`                                        |                                              | Namespace under `ctx.helix` where the mapped response is stored.                                                                                                                     |

A callout is a **success** only when the external service answers with a `2xx` or `3xx` status. A `4xx` or `5xx` response is treated as a **failure** and triggers `error_handling.policy` (so `fail-close` returns `503`, and `fail-open` / `log-only` continue), the same as a connection error.

**Sample configuration**

Make a synchronous GET callout during the access phase:

```json
{
  "service-callout": {
    "uri": "https://profile.internal/lookup",
    "phase": "access",
    "forwarded_headers": ["Authorization"]
  }
}
```

Send a POST callout with a templated JSON body and a custom timeout:

```json
{
  "service-callout": {
    "uri": "https://risk.internal/score",
    "phase": "access",
    "method": "POST",
    "timeout": 5000,
    "custom_headers": {
      "Content-Type": "application/json"
    },
    "request_body_template": "{\"user\":\"$http_x_user_id\",\"path\":\"$uri\"}"
  }
}
```

Send headers and query parameters to the endpoint built from the request context, for example an access token another plugin resolved onto `ctx.helix` and a value from a gateway variable. Each `$ctx.<path>` is replaced with the value at that context path, and `$var` with the gateway variable:

```json
{
  "service-callout": {
    "uri": "https://profile.internal/lookup",
    "phase": "access",
    "method": "GET",
    "custom_headers": {
      "Authorization": "Bearer $ctx.helix.helix_auth.access_token",
      "X-Request-Id": "$http_x_request_id"
    },
    "custom_query_params": {
      "product": "$ctx.helix.helix_auth.product_id"
    }
  }
}
```

Forward the full request object and return 503 if the callout fails:

```json
{
  "service-callout": {
    "uri": "https://audit.internal/ingest",
    "phase": "rewrite",
    "method": "POST",
    "send_request_object": true,
    "ssl_verify": true,
    "error_handling": {
      "policy": "fail-close",
      "message": "audit service unavailable"
    }
  }
}
```

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.digitalapi.ai/api-gateway/plugin-reference/plugins-general.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
