Skip to content

Commit

Permalink
feat(plugin) request termination (#2328, #2051)
Browse files Browse the repository at this point in the history
* feat(plugin): request-termination

A new plug-in that allows a request to be terminated and a specified
HTTP status code and body returned.
This is useful to temporarily return a status page for a service. For
example if the service is unavailable due to scheduled maintenance.
  • Loading branch information
Tieske authored Apr 4, 2017
1 parent 57ad9ff commit 020de3e
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 12 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## [Unreleased][unreleased]

### Added

- Request termination plugin. Allowing to terminate a request with a custom
status and message. Thanks to [Paul Austin](https://github.com/pauldaustin)
for the contribution.
[#2051](https://github.com/Mashape/kong/pull/2051)

## [0.10.1] - 2017/03/27

### Changed
Expand Down
3 changes: 3 additions & 0 deletions kong-0.10.1-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -275,5 +275,8 @@ build = {
["kong.plugins.aws-lambda.handler"] = "kong/plugins/aws-lambda/handler.lua",
["kong.plugins.aws-lambda.schema"] = "kong/plugins/aws-lambda/schema.lua",
["kong.plugins.aws-lambda.v4"] = "kong/plugins/aws-lambda/v4.lua",

["kong.plugins.request-termination.handler"] = "kong/plugins/request-termination/handler.lua",
["kong.plugins.request-termination.schema"] = "kong/plugins/request-termination/schema.lua",
}
}
34 changes: 28 additions & 6 deletions kong/constants.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
local plugins = {
"jwt", "acl", "correlation-id", "cors", "oauth2", "tcp-log", "udp-log",
"file-log", "http-log", "key-auth", "hmac-auth", "basic-auth", "ip-restriction",
"galileo", "request-transformer", "response-transformer",
"request-size-limiting", "rate-limiting", "response-ratelimiting", "syslog",
"loggly", "datadog", "runscope", "ldap-auth", "statsd", "bot-detection",
"aws-lambda"
"jwt",
"acl",
"correlation-id",
"cors",
"oauth2",
"tcp-log",
"udp-log",
"file-log",
"http-log",
"key-auth",
"hmac-auth",
"basic-auth",
"ip-restriction",
"galileo",
"request-transformer",
"response-transformer",
"request-size-limiting",
"rate-limiting",
"response-ratelimiting",
"syslog",
"loggly",
"datadog",
"runscope",
"ldap-auth",
"statsd",
"bot-detection",
"aws-lambda",
"request-termination",
}

local plugin_map = {}
Expand Down
39 changes: 39 additions & 0 deletions kong/plugins/request-termination/handler.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
local BasePlugin = require "kong.plugins.base_plugin"
local responses = require "kong.tools.responses"
local meta = require "kong.meta"

local server_header = meta._NAME.."/"..meta._VERSION

local RequestTerminationHandler = BasePlugin:extend()

RequestTerminationHandler.PRIORITY = 1

function RequestTerminationHandler:new()
RequestTerminationHandler.super.new(self, "request-termination")
end

function RequestTerminationHandler:access(conf)
RequestTerminationHandler.super.access(self)

local status_code = conf.status_code
local content_type = conf.content_type
local body = conf.body
local message = conf.message
if body then
ngx.status = status_code

if not content_type then
content_type = "application/json; charset=utf-8";
end
ngx.header["Content-Type"] = content_type
ngx.header["Server"] = server_header

ngx.say(body)

return ngx.exit(status_code)
else
return responses.send(status_code, message)
end
end

return RequestTerminationHandler
31 changes: 31 additions & 0 deletions kong/plugins/request-termination/schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
local Errors = require "kong.dao.errors"
local utils = require "kong.tools.utils"

return {
no_consumer = true,
fields = {
status_code = { type = "number", default = 503 },
message = { type = "string" },
content_type = { type = "string" },
body = { type = "string" },
},
self_check = function(schema, plugin_t, dao, is_updating)
if plugin_t.status_code then
if plugin_t.status_code < 100 or plugin_t.status_code > 599 then
return false, Errors.schema("status_code must be between 100..599")
end
end

if plugin_t.message then
if plugin_t.content_type or plugin_t.body then
return false, Errors.schema("message cannot be used with content_type or body")
end
else
if plugin_t.content_type and not plugin_t.body then
return false, Errors.schema("content_type requires a body")
end
end

return true
end
}
17 changes: 12 additions & 5 deletions kong/tools/responses.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ local server_header = meta._NAME.."/"..meta._VERSION
-- @field HTTP_CONFLICT 409 Conflict
-- @field HTTP_UNSUPPORTED_MEDIA_TYPE 415 Unsupported Media Type
-- @field HTTP_INTERNAL_SERVER_ERROR Internal Server Error
-- @field HTTP_SERVICE_UNAVAILABLE 503 Service Unavailable
-- @usage return responses.send_HTTP_OK()
-- @usage return responses.HTTP_CREATED("Entity created")
-- @usage return responses.HTTP_INTERNAL_SERVER_ERROR()
Expand All @@ -55,7 +56,8 @@ local _M = {
HTTP_METHOD_NOT_ALLOWED = 405,
HTTP_CONFLICT = 409,
HTTP_UNSUPPORTED_MEDIA_TYPE = 415,
HTTP_INTERNAL_SERVER_ERROR = 500
HTTP_INTERNAL_SERVER_ERROR = 500,
HTTP_SERVICE_UNAVAILABLE = 503,
}
}

Expand All @@ -68,6 +70,7 @@ local _M = {
-- @field status_codes.HTTP_UNAUTHORIZED Default: Unauthorized
-- @field status_codes.HTTP_INTERNAL_SERVER_ERROR Always "Internal Server Error"
-- @field status_codes.HTTP_METHOD_NOT_ALLOWED Always "Method not allowed"
-- @field status_codes.HTTP_SERVICE_UNAVAILABLE Default: "Service unavailable"
local response_default_content = {
[_M.status_codes.HTTP_UNAUTHORIZED] = function(content)
return content or "Unauthorized"
Expand All @@ -83,20 +86,23 @@ local response_default_content = {
end,
[_M.status_codes.HTTP_METHOD_NOT_ALLOWED] = function(content)
return "Method not allowed"
end
end,
[_M.status_codes.HTTP_SERVICE_UNAVAILABLE] = function(content)
return content or "Service unavailable"
end,
}

-- Return a closure which will be usable to respond with a certain status code.
-- @local
-- @param[type=number] status_code The status for which to define a function
local function send_response(status_code)
-- Send a JSON response for the closure's status code with the given content.
-- If the content happens to be an error (>500), it will be logged by ngx.log as an ERR.
-- If the content happens to be an error (500), it will be logged by ngx.log as an ERR.
-- @see https://github.com/openresty/lua-nginx-module
-- @param content (Optional) The content to send as a response.
-- @return ngx.exit (Exit current context)
return function(content, headers)
if status_code >= _M.status_codes.HTTP_INTERNAL_SERVER_ERROR then
if status_code == _M.status_codes.HTTP_INTERNAL_SERVER_ERROR then
if content then
ngx.log(ngx.ERR, tostring(content))
end
Expand Down Expand Up @@ -141,7 +147,8 @@ local closure_cache = {}
--- Send a response with any status code or body,
-- Not all status codes are available as sugar methods, this function can be
-- used to send any response.
-- If the `status_code` parameter is in the 5xx range, it is expectde that the `content` parameter be the error encountered. It will be logged and the response body will be empty. The user will just receive a 500 status code.
-- For `status_code=5xx` the `content` parameter should be the description of the error that occurred.
-- For `status_code=500` the content will be logged by ngx.log as an ERR.
-- Will call `ngx.say` and `ngx.exit`, terminating the current context.
-- @see ngx.say
-- @see ngx.exit
Expand Down
19 changes: 18 additions & 1 deletion spec/01-unit/09-responses_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ describe("Response helpers", function()
end)
end
end)
it("calls `ngx.log` if and only if a 500 status code range was given", function()
it("calls `ngx.log` if and only if a 500 status code was given", function()
responses.send_HTTP_BAD_REQUEST()
assert.stub(ngx.log).was_not_called()

responses.send_HTTP_BAD_REQUEST("error")
assert.stub(ngx.log).was_not_called()

responses.send_HTTP_INTERNAL_SERVER_ERROR()
assert.stub(ngx.log).was_not_called()
Expand All @@ -69,6 +72,14 @@ describe("Response helpers", function()
assert.stub(ngx.log).was_called()
end)

it("don't call `ngx.log` if a 503 status code was given", function()
responses.send_HTTP_SERVICE_UNAVAILABLE()
assert.stub(ngx.log).was_not_called()

responses.send_HTTP_SERVICE_UNAVAILABLE()
assert.stub(ngx.log).was_not_called("error")
end)

describe("default content rules for some status codes", function()
it("should apply default content rules for some status codes", function()
responses.send_HTTP_NOT_FOUND()
Expand All @@ -86,6 +97,12 @@ describe("Response helpers", function()
responses.send_HTTP_INTERNAL_SERVER_ERROR("override")
assert.stub(ngx.say).was.called_with("{\"message\":\"An unexpected error occurred\"}")
end)
it("should apply default content rules for some status codes", function()
responses.send_HTTP_SERVICE_UNAVAILABLE()
assert.stub(ngx.say).was.called_with("{\"message\":\"Service unavailable\"}")
responses.send_HTTP_SERVICE_UNAVAILABLE("override")
assert.stub(ngx.say).was.called_with("{\"message\":\"override\"}")
end)
end)

describe("send()", function()
Expand Down
57 changes: 57 additions & 0 deletions spec/03-plugins/27-request-termination/01-schema_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
local schemas_validation = require "kong.dao.schemas_validation"
local schema = require "kong.plugins.request-termination.schema"

local v = schemas_validation.validate_entity

describe("Plugin: request-termination (schema)", function()
it("should accept a valid status_code", function()
assert(v({status_code = 404}, schema))
end)
it("should accept a valid message", function()
assert(v({message = "Not found"}, schema))
end)
it("should accept a valid content_type", function()
assert(v({content_type = "text/html",body = "<body><h1>Not found</h1>"}, schema))
end)
it("should accept a valid body", function()
assert(v({body = "<body><h1>Not found</h1>"}, schema))
end)

describe("errors", function()
it("status_code should only accept numbers", function()
local ok, err = v({status_code = "abcd"}, schema)
assert.same({status_code = "status_code is not a number"}, err)
assert.False(ok)
end)
it("status_code < 100", function()
local ok, _, err = v({status_code = "99"}, schema)
assert.False(ok)
assert.same("status_code must be between 100..599", err.message)
end)
it("status_code > 599", function()
local ok, _, err = v({status_code = "600"}, schema)
assert.False(ok)
assert.same("status_code must be between 100..599", err.message)
end)
it("message with body", function()
local ok, _, err = v({message = "error", body = "test"}, schema)
assert.False(ok)
assert.same("message cannot be used with content_type or body", err.message)
end)
it("message with body and content_type", function()
local ok, _, err = v({message = "error", content_type="text/html", body = "test"}, schema)
assert.False(ok)
assert.same("message cannot be used with content_type or body", err.message)
end)
it("message with content_type", function()
local ok, _, err = v({message = "error", content_type="text/html"}, schema)
assert.False(ok)
assert.same("message cannot be used with content_type or body", err.message)
end)
it("content_type without body", function()
local ok, _, err = v({content_type="text/html"}, schema)
assert.False(ok)
assert.same("content_type requires a body", err.message)
end)
end)
end)
Loading

0 comments on commit 020de3e

Please sign in to comment.