Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions plugins/wasm-go/extensions/response-cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
## 简介
---
title: 通用响应缓存
keywords: [higress,response cache]
description: 通用响应缓存插件配置参考
---

## 功能说明

通用响应缓存插件,支持从请求头/请求体中提取key,从响应体中提取value并缓存起来;下次请求时,如果请求头/请求体中携带了相同的key,则直接返回缓存中的value,而不会请求后端服务。

**提示**

携带请求头`x-higress-skip-response-cache: on`时,当前请求将不会使用缓存中的内容,而是直接转发给后端服务,同时也不会缓存该请求返回响应的内容


## 运行属性

插件执行阶段:`认证阶段`
插件执行优先级:`10`

## 配置说明
配置包括 缓存数据库(cache)配置部分,以及配置缓存内容部分

## 配置说明

## 缓存服务(cache)
| cache.type | string | required | "" | 缓存服务类型,例如 redis |
| --- | --- | --- | --- | --- |
| cache.serviceName | string | required | "" | 缓存服务名称 |
| cache.serviceHost | string | required | "" | 缓存服务域名 |
| cache.servicePort | int64 | optional | 6379 | 缓存服务端口 |
| cache.username | string | optional | "" | 缓存服务用户名 |
| cache.password | string | optional | "" | 缓存服务密码 |
| cache.timeout | uint32 | optional | 10000 | 缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
| cache.cacheTTL | int | optional | 0 | 缓存过期时间,单位为秒。默认值是 0,即 永不过期|
| cacheKeyPrefix | string | optional | "higress-response-cache:" | 缓存 Key 的前缀,默认值为 "higress-response-cache:" |


## 其他配置
| Name | Type | Requirement | Default | Description |
| --- | --- | --- | --- | --- |
| cacheResponseCode | array of number | optional | 200 | 表示支持缓存的响应状态码列表;默认为200|
| cacheKeyFromHeader | string | optional | "" | 表示提取header中的固定字段的值作为缓存key;配置此项时会从请求头提取key,不会读取请求body;cacheKeyFromHeader和cacheKeyFromBody**非空情况下只支持配置一项**|
| cacheKeyFromBody | string | optional | "" | 配置为空时,表示提取所有body作为缓存key;否则按JSON响应格式,从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串;仅在cacheKeyFromHeader为空或未配置时生效 |
| cacheValueFromBodyType | string | optional | "application/json" | 表示缓存body的类型,命中cache时content-type会返回该值;默认为"application/json" |
| cacheValueFromBody | string | optional | "" | 配置为空时,表示缓存所有body;当cacheValueFromBodyType为"application/json"时,支持从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |

其中,缓存key的拼接逻辑为以下中一个:
1. `cacheKeyPrefix` + 从请求头中`cacheKeyFromHeader`对应字段提取的内容
2. `cacheKeyPrefix` + 从请求体中`cacheKeyFromBody`对应字段提取的内容

**注意**:`cacheKeyFromHeader` 和 `cacheKeyFromBody` 不能同时配置(非空情况下只支持配置一项)。如果同时配置,插件在配置解析阶段会报错。


命中缓存插件的情况下,返回的响应头中有三种状态:
- `x-cache-status: hit` ,表示命中缓存,直接返回缓存内容
- `x-cache-status: miss` ,表示未命中缓存,返回后端响应结果
- `x-cache-status: skip` ,表示跳过缓存检查


## 配置示例
### 基础配置
```yaml
cache:
type: redis
serviceName: my-redis.dns
servicePort: 6379
timeout: 2000

cacheKeyFromHeader: "x-http-cache-key"

cacheValueFromBodyType: "application/json"
cacheValueFromBody: "messages.@reverse.0.content"
```

假设请求为

```bash
# Request
curl -H "x-http-cache-key: abcd" <url>

# Response
{"messages":[{"content":"1"}, {"content":"2"}, {"content":"3"}]}
```

则缓存的key为`higress-response-cache:abcd`,缓存的value为`3`。

后续请求命中缓存时,响应Content-type返回为 `application/json`。


### 响应body作为value

如果缓存所有响应body,则可以配置为

```yaml
cacheValueFromBodyType: "text/html"
cacheValueFromBody: ""

```

后续请求命中缓存时,响应Content-type返回为 `text/html`。

### 请求body作为key

使用请求body作为key,则可以配置为

```yaml
cacheKeyFromBody: ""
```

配置支持GJSON PATH语法。

## 进阶用法
Body为`application/json`时,支持基于 GJSON PATH 语法:

比如表达式:`messages.@reverse.0.content` ,含义是把 messages 数组反转后取第一项的 content;

GJSON PATH 也支持条件判断语法,例如希望取最后一个 role 为 user 的 content 作为 key,可以写成: `messages.@reverse.#(role=="user").content`;

如果希望将所有 role 为 user 的 content 拼成一个数组作为 key,可以写成:`messages.@reverse.#(role=="user")#.content`;

还可以支持管道语法,例如希望取到数第二个 role 为 user 的 content 作为 key,可以写成:`messages.@reverse.#(role=="user")#.content|1`。

更多用法可以参考[官方文档](https://github.com/tidwall/gjson/blob/master/SYNTAX.md),可以使用 [GJSON Playground](https://gjson.dev/) 进行语法测试。

## 常见问题

1. 如果返回的错误为 `error status returned by host: bad argument`,请检查:
- `serviceName`是否正确包含了服务的类型后缀(.dns等)
- `servicePort`配置是否正确,尤其是 `static` 类型的服务端口现在固定为 80
121 changes: 121 additions & 0 deletions plugins/wasm-go/extensions/response-cache/README_EN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
title: Response Cache
keywords: [higress,response cache]
description: Response Cache Plugin Configuration Reference
---
## Function Description
Response caching plugin supports extracting keys from request headers/request bodies and caching values extracted from response bodies. On subsequent requests, if the request headers/request bodies contain the same key, it directly returns the cached value without forwarding the request to the backend service.

**Hint**

When carrying the request header `x-higress-skip-response-cache: on`, the current request will not use content from the cache but will be directly forwarded to the backend service. Additionally, the response content from this request will not be cached.

## Runtime Properties
Plugin Execution Phase: `Authentication Phase`
Plugin Execution Priority: `10`

## Configuration Description

### Cache Service (cache)
| Property | Type | Requirement | Default | Description |
| --- | --- | --- | --- | --- |
| cache.type | string | required | "" | Cache service type, e.g., redis |
| cache.serviceName | string | required | "" | Cache service name |
| cache.serviceHost | string | required | "" | Cache service domain |
| cache.servicePort | int64 | optional | 6379 | Cache service port |
| cache.username | string | optional | "" | Cache service username |
| cache.password | string | optional | "" | Cache service password |
| cache.timeout | uint32 | optional | 10000 | Timeout for cache service in milliseconds. Default is 10000, i.e., 10 seconds |
| cache.cacheTTL | int | optional | 0 | Cache expiration time in seconds. Default is 0, meaning never expires |
| cacheKeyPrefix | string | optional | "higress-response-cache:" | Prefix for cache keys, default is "higress-response-cache:" | |

### Other Configurations
| Name | Type | Requirement | Default | Description |
| --- | --- | --- | --- | --- |
| cacheResponseCode | array of number | optional | 200 | Indicates the list of response status codes that support caching; the default is 200.|
| cacheKeyFromHeader | string | optional | "" | Extracts a fixed field's value from headers as the cache key; when configured, extracts key from request headers without reading the request body; **only one of cacheKeyFromHeader and cacheKeyFromBody can be configured when both are non-empty**|
| cacheKeyFromBody | string | optional | "" | If empty, extracts all body as the cache key; otherwise, extracts a string from the request body in JSON format based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md); only takes effect when cacheKeyFromHeader is empty or not configured |
| cacheValueFromBodyType | string | optional | "application/json" | Indicates the type of cached body; the content-type returned on cache hit will be this value; default is "application/json" |
| cacheValueFromBody | string | optional | "" | If empty, caches all body; when cacheValueFromBodyType is "application/json", supports extracting a string from the response body based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) |


The logic for concatenating the cache key is one of the following:

1. `cacheKeyPrefix` + content extracted from the field corresponding to `cacheKeyFromHeader` in the request header
2. `cacheKeyPrefix` + content extracted from the field corresponding to `cacheKeyFromBody` in the request body

**Note**: `cacheKeyFromHeader` and `cacheKeyFromBody` cannot be configured at the same time (only one of them can be configured when both are non-empty). If both are configured, the plugin will return an error during the configuration parsing phase.

In the case of hitting the cache plugin, there are three statuses in the returned response headers:

- `x-cache-status: hit` , indicating a cache hit and cached content is returned directly
- `x-cache-status: miss` , indicating a cache miss and backend response results are returned
- `x-cache-status: skip` , indicating skipping the cache check

## Configuration Example
### Basic Configuration
```yaml
cache:
type: redis
serviceName: my-redis.dns
servicePort: 6379
timeout: 2000

cacheKeyFromHeader: "x-http-cache-key"

cacheValueFromBodyType: "application/json"
cacheValueFromBody: "messages.@reverse.0.content"
```

Assumed Request

```bash
# Request
curl -H "x-http-cache-key: abcd" <url>

# Response
{"messages":[{"content":"1"}, {"content":"2"}, {"content":"3"}]}
```

In this case, the cache key would be `higress-response-cache:abcd`, and the cached value would be `3`.

For subsequent requests that hit the cache, the response Content-Type returned is `application/json`.

### Response Body as Cache Value
To cache all response bodies, configure as follows:

```yaml
cacheValueFromBodyType: "text/html"
cacheValueFromBody: ""
```
For subsequent requests that hit the cache, the response Content-Type returned is `text/html`.


### Request Body as Cache Key
To use the request body as the key, configure as follows:

```yaml

cacheKeyFromBody: ""
```

The configuration supports GJSON PATH syntax.


## Advanced Usage
When the body is `application/json`, GJSON PATH syntax is supported:

For example, the expression `messages.@reverse.0.content` means taking the content of the first item after reversing the messages array.

GJSON PATH also supports conditional syntax. For instance, to take the content of the last message where role is "user", you can write: `messages.@reverse.#(role=="user").content`.

To concatenate all contents where role is "user" into an array, you can write: `messages.@reverse.#(role=="user")#.content`.

Pipeline syntax is also supported. For example, to take the second content where role is "user", you can write: `messages.@reverse.#(role=="user")#.content|1`.

Refer to the [official documentation](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for more usage examples, and test the syntax using the [GJSON Playground](https://gjson.dev/).

## Common Issues
If the error `error status returned by host: bad argument` occurs, check:
- Whether `serviceName` correctly includes the service type suffix (.dns, etc.)
- Whether `servicePort` is configured correctly, especially that `static` type services now use a fixed port of 80
127 changes: 127 additions & 0 deletions plugins/wasm-go/extensions/response-cache/cache/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package cache

import (
"errors"
"strings"

"github.com/higress-group/wasm-go/pkg/wrapper"
"github.com/tidwall/gjson"
)

const (
PROVIDER_TYPE_REDIS = "redis"
DEFAULT_CACHE_PREFIX = "higress-resp-cache:"
)

type providerInitializer interface {
ValidateConfig(ProviderConfig) error
CreateProvider(ProviderConfig) (Provider, error)
}

var (
providerInitializers = map[string]providerInitializer{
PROVIDER_TYPE_REDIS: &redisProviderInitializer{},
}
)

type ProviderConfig struct {
// @Title zh-CN redis 缓存服务提供者类型
// @Description zh-CN 缓存服务提供者类型,例如 redis
typ string
// @Title zh-CN redis 缓存服务名称
// @Description zh-CN 缓存服务名称
serviceName string
// @Title zh-CN redis 缓存服务端口
// @Description zh-CN 缓存服务端口,默认值为6379
servicePort int
// @Title zh-CN redis 缓存服务地址
// @Description zh-CN Cache 缓存服务地址,非必填
serviceHost string
// @Title zh-CN 缓存服务用户名
// @Description zh-CN 缓存服务用户名,非必填
username string
// @Title zh-CN 缓存服务密码
// @Description zh-CN 缓存服务密码,非必填
password string
// @Title zh-CN 请求超时
// @Description zh-CN 请求缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒
timeout uint32
// @Title zh-CN 缓存过期时间
// @Description zh-CN 缓存过期时间,单位为秒。默认值是0,即永不过期
cacheTTL int
// @Title 缓存 Key 前缀
// @Description 缓存 Key 的前缀,默认值为 "higress-resp-cache:"
cacheKeyPrefix string
}

func (c *ProviderConfig) GetProviderType() string {
return c.typ
}

func (c *ProviderConfig) FromJson(json gjson.Result) {
c.typ = json.Get("type").String()
c.serviceName = json.Get("serviceName").String()
c.servicePort = int(json.Get("servicePort").Int())
if !json.Get("servicePort").Exists() {
if strings.HasSuffix(c.serviceName, ".static") {
// use default logic port which is 80 for static service
c.servicePort = 80
} else {
c.servicePort = 6379
}
}
c.serviceHost = json.Get("serviceHost").String()
c.username = json.Get("username").String()
c.password = json.Get("password").String()
c.timeout = uint32(json.Get("timeout").Int())
if !json.Get("timeout").Exists() {
c.timeout = 10000
}
c.cacheTTL = int(json.Get("cacheTTL").Int())
if !json.Get("cacheTTL").Exists() {
c.cacheTTL = 0
// c.cacheTTL = 3600000
}
if json.Get("cacheKeyPrefix").Exists() {
c.cacheKeyPrefix = json.Get("cacheKeyPrefix").String()
} else {
c.cacheKeyPrefix = DEFAULT_CACHE_PREFIX
}

}

func (c *ProviderConfig) Validate() error {
if c.typ == "" {
return errors.New("cache service type is required")
}
if c.serviceName == "" {
return errors.New("cache service name is required")
}
if c.cacheTTL < 0 {
return errors.New("cache TTL must be greater than or equal to 0")
}
initializer, has := providerInitializers[c.typ]
if !has {
return errors.New("unknown cache service provider type: " + c.typ)
}
if err := initializer.ValidateConfig(*c); err != nil {
return err
}
return nil
}

func CreateProvider(pc ProviderConfig) (Provider, error) {
initializer, has := providerInitializers[pc.typ]
if !has {
return nil, errors.New("unknown provider type: " + pc.typ)
}
return initializer.CreateProvider(pc)
}

type Provider interface {
GetProviderType() string
Init(username string, password string, timeout uint32) error
Get(key string, cb wrapper.RedisResponseCallback) error
Set(key string, value string, cb wrapper.RedisResponseCallback) error
GetCacheKeyPrefix() string
}
Loading
Loading