Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/plugin request termination #2328

Merged
merged 16 commits into from Apr 4, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
@@ -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
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
@@ -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
@@ -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
@@ -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
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
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
@@ -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)