mirror of
https://github.com/alibaba/higress.git
synced 2026-03-01 15:10:56 +08:00
feat: Add response-cache plugin (#3061)
Co-authored-by: mirror58229 <674958229@qq.com>
This commit is contained in:
131
plugins/wasm-go/extensions/response-cache/README.md
Normal file
131
plugins/wasm-go/extensions/response-cache/README.md
Normal file
@@ -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
plugins/wasm-go/extensions/response-cache/README_EN.md
Normal file
121
plugins/wasm-go/extensions/response-cache/README_EN.md
Normal file
@@ -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
plugins/wasm-go/extensions/response-cache/cache/provider.go
vendored
Normal file
127
plugins/wasm-go/extensions/response-cache/cache/provider.go
vendored
Normal file
@@ -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
|
||||
}
|
||||
65
plugins/wasm-go/extensions/response-cache/cache/redis.go
vendored
Normal file
65
plugins/wasm-go/extensions/response-cache/cache/redis.go
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type redisProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (r *redisProviderInitializer) ValidateConfig(cf ProviderConfig) error {
|
||||
if len(cf.serviceName) == 0 {
|
||||
return errors.New("cache service name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisProviderInitializer) CreateProvider(cf ProviderConfig) (Provider, error) {
|
||||
rp := redisProvider{
|
||||
config: cf,
|
||||
client: wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: cf.serviceName,
|
||||
Host: cf.serviceHost,
|
||||
Port: int64(cf.servicePort)}),
|
||||
}
|
||||
err := rp.Init(cf.username, cf.password, cf.timeout)
|
||||
return &rp, err
|
||||
}
|
||||
|
||||
type redisProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.RedisClient
|
||||
}
|
||||
|
||||
func (rp *redisProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_REDIS
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Init(username string, password string, timeout uint32) error {
|
||||
err := rp.client.Init(rp.config.username, rp.config.password, int64(rp.config.timeout))
|
||||
if rp.client.Ready() {
|
||||
log.Info("redis init successfully")
|
||||
} else {
|
||||
log.Error("redis init failed, will try later")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Get(key string, cb wrapper.RedisResponseCallback) error {
|
||||
return rp.client.Get(key, cb)
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Set(key string, value string, cb wrapper.RedisResponseCallback) error {
|
||||
if rp.config.cacheTTL == 0 {
|
||||
return rp.client.Set(key, value, cb)
|
||||
} else {
|
||||
return rp.client.SetEx(key, value, rp.config.cacheTTL, cb)
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *redisProvider) GetCacheKeyPrefix() string {
|
||||
return rp.config.cacheKeyPrefix
|
||||
}
|
||||
89
plugins/wasm-go/extensions/response-cache/config/config.go
Normal file
89
plugins/wasm-go/extensions/response-cache/config/config.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/response-cache/cache"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type PluginConfig struct {
|
||||
cacheProvider cache.Provider
|
||||
cacheProviderConfig cache.ProviderConfig
|
||||
|
||||
CacheKeyFromHeader string
|
||||
CacheKeyFromBody string
|
||||
|
||||
CacheValueFromBodyType string
|
||||
CacheValueFromBody string
|
||||
|
||||
CacheResponseCode []int32
|
||||
}
|
||||
|
||||
func (c *PluginConfig) FromJson(json gjson.Result) {
|
||||
c.cacheProviderConfig.FromJson(json.Get("cache"))
|
||||
c.CacheKeyFromHeader = json.Get("cacheKeyFromHeader").String()
|
||||
c.CacheKeyFromBody = json.Get("cacheKeyFromBody").String()
|
||||
|
||||
c.CacheValueFromBodyType = json.Get("cacheValueFromBodyType").String()
|
||||
if c.CacheValueFromBodyType == "" {
|
||||
c.CacheValueFromBodyType = "application/json"
|
||||
}
|
||||
|
||||
c.CacheValueFromBody = json.Get("cacheValueFromBody").String()
|
||||
|
||||
cacheResponseCode := json.Get("cacheResponseCode").Array()
|
||||
c.CacheResponseCode = make([]int32, 0, len(cacheResponseCode))
|
||||
for _, v := range cacheResponseCode {
|
||||
responseCode, err := strconv.Atoi(v.String())
|
||||
if err != nil || responseCode < 100 || responseCode > 999 {
|
||||
log.Errorf("Skip invalid response_code value: %s", v.String())
|
||||
return
|
||||
}
|
||||
c.CacheResponseCode = append(c.CacheResponseCode, int32(responseCode))
|
||||
}
|
||||
|
||||
if len(c.CacheResponseCode) == 0 {
|
||||
c.CacheResponseCode = []int32{200}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PluginConfig) Validate() error {
|
||||
// cache cannot be empty
|
||||
if c.cacheProviderConfig.GetProviderType() == "" {
|
||||
return fmt.Errorf("cache provider cannot be empty")
|
||||
}
|
||||
|
||||
// if cache provider is configured, validate it
|
||||
if c.cacheProviderConfig.GetProviderType() != "" {
|
||||
if err := c.cacheProviderConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// cache key cannot be all set
|
||||
if c.CacheKeyFromHeader != "" && c.CacheKeyFromBody != "" {
|
||||
return fmt.Errorf("cacheKeyFromHeader and cacheKeyFromBody cannot be all set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *PluginConfig) Complete() error {
|
||||
var err error
|
||||
if c.cacheProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("cache provider is set to %s", c.cacheProviderConfig.GetProviderType())
|
||||
c.cacheProvider, err = cache.CreateProvider(c.cacheProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("cache provider is not configured")
|
||||
c.cacheProvider = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetCacheProvider() cache.Provider {
|
||||
return c.cacheProvider
|
||||
}
|
||||
100
plugins/wasm-go/extensions/response-cache/core.go
Normal file
100
plugins/wasm-go/extensions/response-cache/core.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/response-cache/cache"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/response-cache/config"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/resp"
|
||||
)
|
||||
|
||||
// buildCacheKey constructs the full cache key by combining the prefix with the actual key.
|
||||
func buildCacheKey(provider cache.Provider, key string) string {
|
||||
return provider.GetCacheKeyPrefix() + key
|
||||
}
|
||||
|
||||
// CheckCacheForKey checks if the key is in the cache, or triggers similarity search if not found.
|
||||
func CheckCacheForKey(key string, ctx wrapper.HttpContext, c config.PluginConfig) error {
|
||||
activeCacheProvider := c.GetCacheProvider()
|
||||
if activeCacheProvider == nil {
|
||||
return logAndReturnError("[CheckCacheForKey] no cache provider configured")
|
||||
}
|
||||
|
||||
queryKey := buildCacheKey(activeCacheProvider, key)
|
||||
log.Debugf("[%s] [CheckCacheForKey] querying cache with key: %s", PLUGIN_NAME, queryKey)
|
||||
|
||||
err := activeCacheProvider.Get(queryKey, func(response resp.Value) {
|
||||
handleCacheResponse(key, response, ctx, c)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("[%s] [CheckCacheForKey] failed to retrieve key: %s from cache, error: %v", PLUGIN_NAME, key, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCacheResponse processes cache response and handles cache hits and misses.
|
||||
func handleCacheResponse(key string, response resp.Value, ctx wrapper.HttpContext, c config.PluginConfig) {
|
||||
if err := response.Error(); err == nil && !response.IsNull() {
|
||||
log.Infof("[%s] cache hit for key: %s", PLUGIN_NAME, key)
|
||||
processCacheHit(key, response.String(), ctx, c)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("[%s] [handleCacheResponse] cache miss for key: %s", PLUGIN_NAME, key)
|
||||
if err := response.Error(); err != nil {
|
||||
log.Errorf("[%s] [handleCacheResponse] error retrieving key: %s from cache, error: %v", PLUGIN_NAME, key, err)
|
||||
}
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
|
||||
// processCacheHit handles a successful cache hit.
|
||||
func processCacheHit(key string, response string, ctx wrapper.HttpContext, c config.PluginConfig) {
|
||||
if strings.TrimSpace(response) == "" {
|
||||
log.Warnf("[%s] [processCacheHit] cached response for key %s is empty", PLUGIN_NAME, key)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[%s] [processCacheHit] cached response for key %s: %s", PLUGIN_NAME, key, response)
|
||||
|
||||
ctx.SetContext(CACHE_KEY_CONTEXT_KEY, nil)
|
||||
|
||||
contentType := c.CacheValueFromBodyType
|
||||
headers := [][2]string{
|
||||
{"content-type", contentType},
|
||||
{"x-cache-status", "hit"},
|
||||
}
|
||||
|
||||
proxywasm.SendHttpResponseWithDetail(200, "response-cache.hit", headers, []byte(response), -1)
|
||||
|
||||
}
|
||||
|
||||
// logAndReturnError logs an error and returns it.
|
||||
func logAndReturnError(message string) error {
|
||||
message = fmt.Sprintf("[%s] %s", PLUGIN_NAME, message)
|
||||
log.Errorf(message)
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
// Caches the response value
|
||||
func cacheResponse(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
log.Warnf("[%s] [cacheResponse] cached value for key %s is empty", PLUGIN_NAME, key)
|
||||
return
|
||||
}
|
||||
|
||||
activeCacheProvider := c.GetCacheProvider()
|
||||
if activeCacheProvider != nil {
|
||||
queryKey := buildCacheKey(activeCacheProvider, key)
|
||||
_ = activeCacheProvider.Set(queryKey, value, nil)
|
||||
log.Debugf("[%s] [cacheResponse] cache set success, key: %s, length of value: %d", PLUGIN_NAME, queryKey, len(value))
|
||||
}
|
||||
}
|
||||
27
plugins/wasm-go/extensions/response-cache/go.mod
Normal file
27
plugins/wasm-go/extensions/response-cache/go.mod
Normal file
@@ -0,0 +1,27 @@
|
||||
// File generated by hgctl. Modify as required.
|
||||
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/response-cache
|
||||
|
||||
go 1.24.1
|
||||
|
||||
toolchain go1.24.5
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.3-0.20251011083635-792cb1547bac
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
// github.com/weaviate/weaviate-go-client/v4 v4.15.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
32
plugins/wasm-go/extensions/response-cache/go.sum
Normal file
32
plugins/wasm-go/extensions/response-cache/go.sum
Normal file
@@ -0,0 +1,32 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2 h1:8fQqR+wHts8tP+v7GYxmsCNyW5nAjn9wPYV0/+Seqzg=
|
||||
github.com/higress-group/wasm-go v1.0.2/go.mod h1:882/J8ccU4i+LeyFKmeicbHWAYLj8y7YZr60zk0OOCI=
|
||||
github.com/higress-group/wasm-go v1.0.3-0.20251011083635-792cb1547bac h1:tdJzS56Xa6BSHAi9P2omvb98bpI8qFGg6jnCPtPmDgA=
|
||||
github.com/higress-group/wasm-go v1.0.3-0.20251011083635-792cb1547bac/go.mod h1:B8C6+OlpnyYyZUBEdUXA7tYZYD+uwZTNjfkE5FywA+A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
202
plugins/wasm-go/extensions/response-cache/main.go
Normal file
202
plugins/wasm-go/extensions/response-cache/main.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// 这个文件中主要将OnHttpRequestHeaders、OnHttpRequestBody、OnHttpResponseHeaders、OnHttpResponseBody这四个函数实现
|
||||
// 其中的缓存思路调用cache.go中的逻辑
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/response-cache/config"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_NAME = "response-cache"
|
||||
CACHE_KEY_CONTEXT_KEY = "cacheKey"
|
||||
SKIP_CACHE_HEADER = "x-higress-skip-response-cache"
|
||||
|
||||
DEFAULT_MAX_BODY_BYTES uint32 = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
// CreateClient()
|
||||
wrapper.SetCtx(
|
||||
PLUGIN_NAME,
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBody(onHttpRequestBody),
|
||||
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||
wrapper.ProcessResponseBody(onHttpResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, c *config.PluginConfig) error {
|
||||
c.FromJson(json)
|
||||
if err := c.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.Complete(); err != nil {
|
||||
log.Errorf("complete config failed: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, c config.PluginConfig) types.Action {
|
||||
skipCache, _ := proxywasm.GetHttpRequestHeader(SKIP_CACHE_HEADER)
|
||||
if skipCache == "on" {
|
||||
ctx.SetContext(SKIP_CACHE_HEADER, struct{}{})
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
// cache from request header
|
||||
if c.CacheKeyFromHeader != "" {
|
||||
key, _ := proxywasm.GetHttpRequestHeader(c.CacheKeyFromHeader)
|
||||
if key == "" {
|
||||
log.Warnf("[onHttpRequestHeaders] cache key from header: %s is empty, skip cache", c.CacheKeyFromHeader)
|
||||
// Set skip cache flag to skip response processing
|
||||
ctx.SetContext(SKIP_CACHE_HEADER, struct{}{})
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Debugf("[onHttpRequestHeaders] cache key from request header: %s, key: %s", c.CacheKeyFromHeader, key)
|
||||
|
||||
ctx.SetContext(CACHE_KEY_CONTEXT_KEY, key)
|
||||
|
||||
if err := CheckCacheForKey(key, ctx, c); err != nil {
|
||||
log.Errorf("[onHttpRequestHeaders] check cache for key: %s failed, error: %v", key, err)
|
||||
}
|
||||
ctx.DisableReroute()
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
// cache from request body but does not have a body or not application/json format
|
||||
contentType, _ := proxywasm.GetHttpRequestHeader("content-type")
|
||||
|
||||
if contentType == "" || !strings.Contains(contentType, "application/json") {
|
||||
log.Warnf("[onHttpRequestHeaders] content is not application/json, can't process: %s", contentType)
|
||||
// Set skip cache flag to skip response processing
|
||||
ctx.SetContext(SKIP_CACHE_HEADER, struct{}{})
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
ctx.SetRequestBodyBufferLimit(DEFAULT_MAX_BODY_BYTES)
|
||||
|
||||
ctx.DisableReroute()
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
// The request has a body and requires delaying the header transmission until a cache miss occurs,
|
||||
// at which point the header should be sent.
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []byte) types.Action {
|
||||
var key string
|
||||
if c.CacheKeyFromBody != "" {
|
||||
bodyJson := gjson.ParseBytes(body)
|
||||
|
||||
log.Debugf("[onHttpRequestBody] cache key from requestBody: %s", c.CacheKeyFromBody)
|
||||
|
||||
key = bodyJson.Get(c.CacheKeyFromBody).String()
|
||||
|
||||
if key == "" {
|
||||
log.Debug("[onHttpRequestBody] parse key from request body failed")
|
||||
// Set skip cache flag to skip response processing
|
||||
ctx.SetContext(SKIP_CACHE_HEADER, struct{}{})
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
} else {
|
||||
key = string(body)
|
||||
log.Debugf("[onHttpRequestBody] cache key from requestWholeBody.")
|
||||
}
|
||||
|
||||
log.Debugf("[onHttpRequestBody] key: %s", key)
|
||||
ctx.SetContext(CACHE_KEY_CONTEXT_KEY, key)
|
||||
|
||||
if err := CheckCacheForKey(key, ctx, c); err != nil {
|
||||
log.Errorf("[onHttpRequestBody] check cache for key: %s failed, error: %v", key, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, c config.PluginConfig) types.Action {
|
||||
status, err := proxywasm.GetHttpResponseHeader(":status")
|
||||
if err != nil {
|
||||
log.Errorf("[onHttpResponseBody] unable to load :status header from response: %v", err)
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
// 状态码判断
|
||||
found := false
|
||||
respCode, _ := strconv.Atoi(status)
|
||||
for _, element := range c.CacheResponseCode {
|
||||
if element == int32(respCode) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Infof("[onHttpResponseBody] status not allow to cached: %s", status)
|
||||
proxywasm.AddHttpResponseHeader("x-cache-status", "skip")
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
skipCache := ctx.GetContext(SKIP_CACHE_HEADER)
|
||||
if skipCache != nil {
|
||||
proxywasm.AddHttpResponseHeader("x-cache-status", "skip")
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
if ctx.GetContext(CACHE_KEY_CONTEXT_KEY) != nil {
|
||||
proxywasm.AddHttpResponseHeader("x-cache-status", "miss")
|
||||
}
|
||||
ctx.SetResponseBodyBufferLimit(DEFAULT_MAX_BODY_BYTES)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, c config.PluginConfig, body []byte) types.Action {
|
||||
key := ctx.GetContext(CACHE_KEY_CONTEXT_KEY)
|
||||
if key == nil {
|
||||
log.Debug("[onHttpResponseBody] key is nil, skip cache")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
var value string
|
||||
if c.CacheValueFromBody != "" {
|
||||
if strings.Contains(c.CacheValueFromBodyType, "application/json") {
|
||||
// cache application/json parse response body
|
||||
bodyJson := gjson.ParseBytes(body)
|
||||
if !bodyJson.Exists() {
|
||||
log.Warnf("[onHttpResponseBody] parse application/json from non application/json response body: %s", body)
|
||||
return types.ActionContinue
|
||||
}
|
||||
value = bodyJson.Get(c.CacheValueFromBody).String()
|
||||
if strings.TrimSpace(value) == "" {
|
||||
log.Warnf("[onHttpResponseBody] parse value from response body failed, body:%s", body)
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
//If there are other body types, add a parsing process here.
|
||||
} else {
|
||||
value = string(body)
|
||||
}
|
||||
|
||||
cacheResponse(ctx, c, key.(string), value)
|
||||
return types.ActionContinue
|
||||
|
||||
}
|
||||
703
plugins/wasm-go/extensions/response-cache/main_test.go
Normal file
703
plugins/wasm-go/extensions/response-cache/main_test.go
Normal file
@@ -0,0 +1,703 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/response-cache/config"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:使用header提取key
|
||||
var configWithHeaderKey = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"cache": map[string]interface{}{
|
||||
"type": "redis",
|
||||
"serviceName": "redis.static",
|
||||
"servicePort": 6379,
|
||||
"timeout": 10000,
|
||||
},
|
||||
"cacheKeyFromHeader": "x-user-id",
|
||||
"cacheValueFromBody": "data",
|
||||
"cacheValueFromBodyType": "application/json",
|
||||
"cacheResponseCode": []int{200},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:使用body提取key
|
||||
var configWithBodyKey = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"cache": map[string]interface{}{
|
||||
"type": "redis",
|
||||
"serviceName": "redis.static",
|
||||
"servicePort": 6379,
|
||||
"timeout": 10000,
|
||||
},
|
||||
"cacheKeyFromBody": "user_id",
|
||||
"cacheValueFromBody": "message.content",
|
||||
"cacheValueFromBodyType": "application/json",
|
||||
"cacheResponseCode": []int{200},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:使用整个body作为key
|
||||
var configWithBodyAsKey = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"cache": map[string]interface{}{
|
||||
"type": "redis",
|
||||
"serviceName": "redis.static",
|
||||
"servicePort": 6379,
|
||||
"timeout": 10000,
|
||||
},
|
||||
"cacheKeyFromBody": "",
|
||||
"cacheValueFromBody": "",
|
||||
"cacheValueFromBodyType": "application/json",
|
||||
"cacheResponseCode": []int{200},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:配置冲突(同时设置header和body key)
|
||||
var configConflict = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"cache": map[string]interface{}{
|
||||
"type": "redis",
|
||||
"serviceName": "redis.static",
|
||||
"servicePort": 6379,
|
||||
},
|
||||
"cacheKeyFromHeader": "x-user-id",
|
||||
"cacheKeyFromBody": "user_id",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:最小配置
|
||||
var minimalConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"cache": map[string]interface{}{
|
||||
"type": "redis",
|
||||
"serviceName": "redis.static",
|
||||
"servicePort": 6379,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少cache provider
|
||||
var configMissingCache = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"cacheKeyFromHeader": "x-user-id",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试header key配置
|
||||
t.Run("config with header key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
configRaw, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, configRaw)
|
||||
|
||||
cfg, ok := configRaw.(*config.PluginConfig)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "x-user-id", cfg.CacheKeyFromHeader)
|
||||
require.Equal(t, "", cfg.CacheKeyFromBody)
|
||||
require.Equal(t, "data", cfg.CacheValueFromBody)
|
||||
require.Equal(t, []int32{200}, cfg.CacheResponseCode)
|
||||
})
|
||||
|
||||
// 测试body key配置
|
||||
t.Run("config with body key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
configRaw, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, configRaw)
|
||||
|
||||
cfg, ok := configRaw.(*config.PluginConfig)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "", cfg.CacheKeyFromHeader)
|
||||
require.Equal(t, "user_id", cfg.CacheKeyFromBody)
|
||||
require.Equal(t, "message.content", cfg.CacheValueFromBody)
|
||||
})
|
||||
|
||||
// 测试整个body作为key
|
||||
t.Run("config with body as key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyAsKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
configRaw, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, configRaw)
|
||||
|
||||
cfg, ok := configRaw.(*config.PluginConfig)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "", cfg.CacheKeyFromHeader)
|
||||
require.Equal(t, "", cfg.CacheKeyFromBody)
|
||||
})
|
||||
|
||||
// 测试配置冲突
|
||||
t.Run("conflict config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configConflict)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试缺少cache provider
|
||||
t.Run("missing cache provider", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configMissingCache)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试最小配置
|
||||
t.Run("minimal config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
configRaw, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, configRaw)
|
||||
|
||||
cfg, ok := configRaw.(*config.PluginConfig)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, []int32{200}, cfg.CacheResponseCode)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试使用header key的请求头处理
|
||||
t.Run("request headers with header key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含cache key
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,因为从header提取key后继续处理
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试header key为空
|
||||
t.Run("request headers with empty header key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含x-user-id
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,跳过缓存
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试skip cache header
|
||||
t.Run("skip cache header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置跳过缓存的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
{"x-higress-skip-response-cache", "on"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试使用body key的content-type检查
|
||||
t.Run("request headers for body key with content type", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含application/json content-type
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration,等待读取body
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
})
|
||||
|
||||
// 测试content-type不匹配
|
||||
t.Run("request headers with non-json content type", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,content-type不是json
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,跳过缓存
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试无content-type
|
||||
t.Run("request headers without content type", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,无content-type
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,跳过缓存
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试从body提取key
|
||||
t.Run("request body with key extraction", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造请求体
|
||||
requestBody := `{"user_id": "user123", "data": "test"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,等待缓存检查结果
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
})
|
||||
|
||||
// 测试从body提取key失败(key为空)
|
||||
t.Run("request body with empty key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造请求体,不包含user_id字段
|
||||
requestBody := `{"data": "test"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionContinue,跳过缓存
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试整个body作为key
|
||||
t.Run("request body as key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyAsKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造请求体
|
||||
requestBody := `{"data": "test"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试响应头处理 - 状态码200
|
||||
t.Run("response headers with 200 status", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试响应头处理 - 状态码500(不支持缓存)
|
||||
t.Run("response headers with 500 status", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
})
|
||||
|
||||
// 设置响应头,状态码500
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "500"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,但跳过缓存
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试skip cache header的处理
|
||||
t.Run("response headers with skip cache", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含skip cache标志
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
{"x-higress-skip-response-cache", "on"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试响应体处理 - 提取特定字段
|
||||
t.Run("response body with value extraction", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造响应体
|
||||
responseBody := `{"data": "cached value", "other": "ignored"}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试响应体处理 - 整个body作为value
|
||||
t.Run("response body as value", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyAsKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
host.CallOnHttpRequestBody([]byte(`{"test": "data"}`))
|
||||
|
||||
// 设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造响应体
|
||||
responseBody := `{"data": "full response"}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试无key的响应体处理
|
||||
t.Run("response body without key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头,不经过请求处理
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造响应体
|
||||
responseBody := `{"data": "test"}`
|
||||
host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 测试缓存命中流程
|
||||
func TestCacheHitFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试完整的缓存命中流程
|
||||
t.Run("complete cache hit flow with header key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
})
|
||||
|
||||
// 模拟Redis缓存命中 - 返回之前缓存的data字段值
|
||||
cacheHitResp := test.CreateRedisRespString("cached value")
|
||||
host.CallOnRedisCall(0, cacheHitResp)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
|
||||
// 验证缓存命中的响应
|
||||
localResp := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), localResp.StatusCode)
|
||||
require.Equal(t, "cached value", string(localResp.Data))
|
||||
require.True(t, test.HasHeaderWithValue(localResp.Headers, "content-type", "application/json"))
|
||||
require.True(t, test.HasHeaderWithValue(localResp.Headers, "x-cache-status", "hit"))
|
||||
})
|
||||
|
||||
// 测试缓存未命中然后存储的流程
|
||||
t.Run("cache miss and store flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
})
|
||||
|
||||
// 模拟Redis缓存未命中(返回null)
|
||||
cacheMissResp := test.CreateRedisRespNull()
|
||||
host.CallOnRedisCall(0, cacheMissResp)
|
||||
|
||||
// 设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置响应体
|
||||
responseBody := `{"data": "new data", "other": "ignored"}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 模拟Redis存储操作(SET操作返回OK)
|
||||
storeResp := test.CreateRedisRespArray([]interface{}{"OK"})
|
||||
host.CallOnRedisCall(0, storeResp)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试两次请求:第一次miss,第二次hit
|
||||
t.Run("first request miss then second request hit", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithHeaderKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// ========== 第一次请求:缓存未命中 ==========
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"},
|
||||
})
|
||||
|
||||
// 模拟Redis缓存未命中(第一次查询返回null)
|
||||
cacheMissResp := test.CreateRedisRespNull()
|
||||
host.CallOnRedisCall(0, cacheMissResp)
|
||||
|
||||
// 设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置响应体
|
||||
responseBody := `{"data": "first response"}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 模拟Redis SET操作(第一次请求后将数据存入缓存)
|
||||
storeResp := test.CreateRedisRespArray([]interface{}{"OK"})
|
||||
host.CallOnRedisCall(0, storeResp)
|
||||
host.CompleteHttp()
|
||||
|
||||
// 设置请求头(相同的x-user-id)
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"x-user-id", "user123"}, // 相同的user ID
|
||||
})
|
||||
|
||||
// 模拟Redis缓存命中(第二次查询返回缓存的数据)
|
||||
cacheHitResp := test.CreateRedisRespString("first response")
|
||||
host.CallOnRedisCall(0, cacheHitResp)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
|
||||
// 验证第二次请求返回的是缓存的数据
|
||||
localResp := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), localResp.StatusCode)
|
||||
require.Equal(t, "first response", string(localResp.Data))
|
||||
require.True(t, test.HasHeaderWithValue(localResp.Headers, "x-cache-status", "hit"))
|
||||
require.True(t, test.HasHeaderWithValue(localResp.Headers, "content-type", "application/json"))
|
||||
})
|
||||
|
||||
// 测试body key的两次请求流程
|
||||
t.Run("body key first miss then second hit", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(configWithBodyKey)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 第一次请求
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
requestBody := `{"user_id": "user123"}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// Redis缓存未命中
|
||||
cacheMissResp := test.CreateRedisRespNull()
|
||||
host.CallOnRedisCall(0, cacheMissResp)
|
||||
|
||||
// 响应
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
responseBody := `{"message": {"content": "hello world"}}`
|
||||
host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
// 存储到Redis
|
||||
storeResp := test.CreateRedisRespArray([]interface{}{"OK"})
|
||||
host.CallOnRedisCall(0, storeResp)
|
||||
host.CompleteHttp()
|
||||
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
host.CallOnHttpRequestBody([]byte(`{"user_id": "user123"}`))
|
||||
|
||||
// 缓存命中
|
||||
cacheHitResp := test.CreateRedisRespString("hello world")
|
||||
host.CallOnRedisCall(0, cacheHitResp)
|
||||
host.CompleteHttp()
|
||||
|
||||
// 验证第二次请求返回的是缓存的数据
|
||||
localResp := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), localResp.StatusCode)
|
||||
require.Equal(t, "hello world", string(localResp.Data))
|
||||
require.True(t, test.HasHeaderWithValue(localResp.Headers, "x-cache-status", "hit"))
|
||||
require.True(t, test.HasHeaderWithValue(localResp.Headers, "content-type", "application/json"))
|
||||
})
|
||||
})
|
||||
}
|
||||
52
plugins/wasm-go/extensions/response-cache/option.yaml
Normal file
52
plugins/wasm-go/extensions/response-cache/option.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
# File generated by hgctl. Modify as required.
|
||||
|
||||
version: 1.0.0
|
||||
|
||||
build:
|
||||
# The official builder image version
|
||||
builder:
|
||||
go: 1.19
|
||||
tinygo: 0.28.1
|
||||
oras: 1.0.0
|
||||
# The WASM plugin project directory
|
||||
input: ./
|
||||
# The output of the build products
|
||||
output:
|
||||
# Choose between 'files' and 'image'
|
||||
type: files
|
||||
# Destination address: when type=files, specify the local directory path, e.g., './out' or
|
||||
# type=image, specify the remote docker repository, e.g., 'docker.io/<your_username>/<your_image>'
|
||||
dest: ./out
|
||||
# The authentication configuration for pushing image to the docker repository
|
||||
docker-auth: ~/.docker/config.json
|
||||
# The directory for the WASM plugin configuration structure
|
||||
model-dir: ./
|
||||
# The WASM plugin configuration structure name
|
||||
model: PluginConfig
|
||||
# Enable debug mode
|
||||
debug: false
|
||||
|
||||
test:
|
||||
# Test environment name, that is a docker compose project name
|
||||
name: wasm-test
|
||||
# The output path to build products, that is the source of test configuration parameters
|
||||
from-path: ./out
|
||||
# The test configuration source
|
||||
test-path: ./test
|
||||
# Docker compose configuration, which is empty, looks for the following files from 'test-path':
|
||||
# compose.yaml, compose.yml, docker-compose.yml, docker-compose.yaml
|
||||
compose-file:
|
||||
# Detached mode: Run containers in the background
|
||||
detach: false
|
||||
|
||||
install:
|
||||
# The namespace of the installation
|
||||
namespace: higress-system
|
||||
# Use to validate WASM plugin configuration when install by yaml
|
||||
spec-yaml: ./out/spec.yaml
|
||||
# Installation source. Choose between 'from-yaml' and 'from-go-project'
|
||||
from-yaml: ./test/plugin-conf.yaml
|
||||
# If 'from-go-src' is non-empty, the output type of the build option must be 'image'
|
||||
from-go-src:
|
||||
# Enable debug mode
|
||||
debug: false
|
||||
Reference in New Issue
Block a user