├── handler.lua ├── kong-plugin-response-cache-1.0-1.rockspec ├── readme.md └── schema.lua /handler.lua: -------------------------------------------------------------------------------- 1 | local BasePlugin = require "kong.plugins.base_plugin" 2 | local CacheHandler = BasePlugin:extend() 3 | local responses = require "kong.tools.responses" 4 | local req_get_method = ngx.req.get_method 5 | 6 | local redis = require "resty.redis" 7 | local header_filter = require "kong.plugins.response-transformer.header_transformer" 8 | local is_json_body = header_filter.is_json_body 9 | 10 | local cjson_decode = require("cjson").decode 11 | local cjson_encode = require("cjson").encode 12 | 13 | local function cacheable_request(method, uri, conf) 14 | if method ~= "GET" then 15 | return false 16 | end 17 | 18 | for _,v in ipairs(conf.cache_policy.uris) do 19 | if string.match(uri, "^"..v.."$") then 20 | return true 21 | end 22 | end 23 | 24 | return false 25 | end 26 | 27 | local function get_cache_key(uri, headers, query_params, conf) 28 | local cache_key = uri 29 | 30 | table.sort(query_params) 31 | for _,param in ipairs(conf.cache_policy.vary_by_query_string_parameters) do 32 | local query_value = query_params[param] 33 | if query_value then 34 | if type(query_value) == "table" then 35 | table.sort(query_value) 36 | query_value = table.concat(query_value, ",") 37 | end 38 | ngx.log(ngx.NOTICE, "varying cache key by query string ("..param..":"..query_value..")") 39 | cache_key = cache_key..":"..param.."="..query_value 40 | end 41 | end 42 | 43 | table.sort(headers) 44 | for _,header in ipairs(conf.cache_policy.vary_by_headers) do 45 | local header_value = headers[header] 46 | if header_value then 47 | if type(header_value) == "table" then 48 | table.sort(header_value) 49 | header_value = table.concat(header_value, ",") 50 | end 51 | ngx.log(ngx.NOTICE, "varying cache key by matched header ("..header..":"..header_value..")") 52 | cache_key = cache_key..":"..header.."="..header_value 53 | end 54 | end 55 | 56 | return cache_key 57 | end 58 | 59 | local function json_decode(json) 60 | if json then 61 | local status, res = pcall(cjson_decode, json) 62 | if status then 63 | return res 64 | end 65 | end 66 | end 67 | 68 | local function json_encode(table) 69 | if table then 70 | local status, res = pcall(cjson_encode, table) 71 | if status then 72 | return res 73 | end 74 | end 75 | end 76 | 77 | local function connect_to_redis(conf) 78 | local red = redis:new() 79 | 80 | red:set_timeout(conf.redis_timeout) 81 | 82 | local ok, err = red:connect(conf.redis_host, conf.redis_port) 83 | if err then 84 | return nil, err 85 | end 86 | 87 | if conf.redis_password and conf.redis_password ~= "" then 88 | local ok, err = red:auth(conf.redis_password) 89 | if err then 90 | return nil, err 91 | end 92 | end 93 | 94 | return red 95 | end 96 | 97 | local function red_set(premature, key, val, conf) 98 | local red, err = connect_to_redis(conf) 99 | if err then 100 | ngx_log(ngx.ERR, "failed to connect to Redis: ", err) 101 | end 102 | 103 | red:init_pipeline() 104 | red:set(key, val) 105 | if conf.cache_policy.duration_in_seconds then 106 | red:expire(key, conf.cache_policy.duration_in_seconds) 107 | end 108 | local results, err = red:commit_pipeline() 109 | if err then 110 | ngx_log(ngx.ERR, "failed to commit the pipelined requests: ", err) 111 | end 112 | end 113 | 114 | function CacheHandler:new() 115 | CacheHandler.super.new(self, "response-cache") 116 | end 117 | 118 | function CacheHandler:access(conf) 119 | CacheHandler.super.access(self) 120 | 121 | local uri = ngx.var.uri 122 | if not cacheable_request(req_get_method(), uri, conf) then 123 | ngx.log(ngx.NOTICE, "not cacheable") 124 | return 125 | end 126 | 127 | local cache_key = get_cache_key(uri, ngx.req.get_headers(), ngx.req.get_uri_args(), conf) 128 | local red, err = connect_to_redis(conf) 129 | if err then 130 | ngx_log(ngx.ERR, "failed to connect to Redis: ", err) 131 | return 132 | end 133 | 134 | local cached_val, err = red:get(cache_key) 135 | if cached_val and cached_val ~= ngx.null then 136 | ngx.log(ngx.NOTICE, "cache hit") 137 | local val = json_decode(cached_val) 138 | for k,v in pairs(val.headers) do 139 | ngx.req.set_header(k, v) 140 | end 141 | return responses.send_HTTP_OK(val.content) 142 | end 143 | 144 | ngx.log(ngx.NOTICE, "cache miss") 145 | ngx.ctx.response_cache = { 146 | cache_key = cache_key 147 | } 148 | end 149 | 150 | function CacheHandler:header_filter(conf) 151 | CacheHandler.super.header_filter(self) 152 | 153 | local ctx = ngx.ctx.response_cache 154 | if not ctx then 155 | return 156 | end 157 | 158 | ctx.headers = ngx.resp.get_headers() 159 | end 160 | 161 | function CacheHandler:body_filter(conf) 162 | CacheHandler.super.body_filter(self) 163 | 164 | local ctx = ngx.ctx.response_cache 165 | if not ctx then 166 | return 167 | end 168 | 169 | local chunk = ngx.arg[1] 170 | local eof = ngx.arg[2] 171 | 172 | local res_body = ctx and ctx.res_body or "" 173 | res_body = res_body .. (chunk or "") 174 | ctx.res_body = res_body 175 | if eof then 176 | local content = json_decode(ctx.res_body) 177 | local value = { content = content, headers = ctx.headers } 178 | local value_json = json_encode(value) 179 | ngx.timer.at(0, red_set, ctx.cache_key, value_json, conf) 180 | end 181 | end 182 | 183 | return CacheHandler -------------------------------------------------------------------------------- /kong-plugin-response-cache-1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-plugin-response-cache" 2 | version = "1.0-1" 3 | source = { 4 | url = "http://github.com/wshirey/kong-plugin-response-cache" 5 | } 6 | description = { 7 | summary = "A Kong plugin that will cache responses in redis", 8 | license = "MIT" 9 | } 10 | dependencies = { 11 | "lua ~> 5.1" 12 | } 13 | build = { 14 | type = "builtin", 15 | modules = { 16 | ["kong.plugins.response-cache.handler"] = "handler.lua", 17 | ["kong.plugins.response-cache.schema"] = "schema.lua" 18 | } 19 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # kong-plugin-response-cache 2 | 3 | A Kong plugin that will cache JSON responses in redis 4 | 5 | ## How it works 6 | When enabled, this plugin will cache JSON response bodies and headers that match the 7 | specified URI list into redis. The duration for the cached response is set in 8 | redis and Kong will continue to use the cached response until redis removes it. 9 | 10 | The plugin will only cache JSON responses for GET request methods. 11 | 12 | ## Cache Key computation 13 | 14 | The cache key will be a concatentation of the following items, in order, each delimited 15 | with the `:` character 16 | 17 | 1. The URI path 18 | 1. Query parameter and value (if defined in `config.vary_by_query_parameters`) 19 | 1. Header name and value (if defined in `config.vary_by_headers`) 20 | 21 | Query strings and headers with multiple values will have those values concatenated 22 | and command delimited. See the table below for some examples of requests and 23 | their corresponding cache key. 24 | 25 | Query strings and headers are concatenated in the cache key in alphabetical order. 26 | 27 | request|cache key 28 | ---|--- 29 | `curl /v1/users/wshirey?is_active`|`/v1/users/wshirey:is_active=true` 30 | `curl /v1/users/wshirey?foo=bar&is_active`|`/v1/users/wshirey:foo=bar:is_active=true` 31 | `curl /v1/users/wshirey?foo=bar&foo=baz`|`/v1/users/wshirey:foo=bar,baz` 32 | `curl -H "X-Custom-ID: 123" /v1/users/wshirey?is_active=true`|`/v1/users/wshirey:is_active=true:x-custom-id=123` 33 | `curl -H "X-Custom-ID: 123" -H "X-User-ID: 456" /v1/users/wshirey?is_active=true`|`/v1/users/wshirey:is_active=true:x-custom-id=123:x-user-id=456` 34 | `curl -H "X-Custom-ID: 123" -H "X-Custom-ID: 456" /v1/users/wshirey?is_active=true`|`/v1/users/wshirey:is_active=true:x-custom-id=123,456` 35 | 36 | ## Configuration 37 | 38 | Similar to the built-in JWT Kong plugin, you can associate the jwt-claims-headers 39 | plugin with an api with the following request 40 | 41 | ```bash 42 | curl -X POST http://kong:8001/apis/{api_name_or_id}/plugins \ 43 | --data "name=response-cache" \ 44 | --data "config.cache_policy.uris=/echo,/headers" \ 45 | --data "config.cache_policy.vary_by_query_parameters=" \ 46 | --data "config.cache_policy.vary_by_headers=X-Custom-ID" \ 47 | --data "config.cache_policy.duration_in_seconds=3600" \ 48 | --data "config.redis_host=127.0.0.1" \ 49 | ``` 50 | 51 | form parameter|required|description 52 | ---|---|--- 53 | `name`|*required*|The name of the plugin to use, in this case: `response-cache` 54 | `cache_policy.uris`|*required*|A comma delimited list of URIs that Kong will cache responses. Supports regular expressions. 55 | `cache_policy.vary_by_query_parameters`|*optional*|A comma delimited list of query parameters to use to compute cache key. 56 | `cache_policy.vary_by_headers`|*optional*|A comma delimited list of headers to use to compute cache key. 57 | `cache_policy.duration_in_seconds`|*required*|The amount of time in seconds that a response will be cached in redis (using the redis [EXPIRE](https://redis.io/commands/expire) command). Redis will be responsible from removing cached responses. 58 | `redis_host`|*required*|The hostname or IP address of the redis server. 59 | `redis_timeout`|*required*|The timeout in milliseconds for the redis connection. Defaults to 2000 milliseconds. 60 | `redis_port`|*optional*|The port of the redis server. Defaults to 6379. 61 | `redis_password`|*optional*|The password (if required) to authenticate to the redis server. 62 | -------------------------------------------------------------------------------- /schema.lua: -------------------------------------------------------------------------------- 1 | return { 2 | no_consumer = true, 3 | fields = { 4 | cache_policy = { 5 | type = "table", 6 | schema = { 7 | fields = { 8 | uris = { type = "array", required = true }, 9 | vary_by_query_string_parameters = { type = "array", default = {} }, 10 | vary_by_headers = { type = "array", default = {} }, 11 | duration_in_seconds = { type = "string", required = true } 12 | } 13 | } 14 | }, 15 | redis_host = { type = "string", required = true }, 16 | redis_port = { type = "number", default = 6379 }, 17 | redis_password = { type = "string" }, 18 | redis_timeout = { type = "number", default = 2000 } 19 | } 20 | } --------------------------------------------------------------------------------