kong/kong/pdk/response.lua (632 lines of code) (raw):
---
-- Client response module.
--
-- The downstream response module contains a set of functions for producing and
-- manipulating responses sent back to the client (downstream). Responses can
-- be produced by Kong (for example, an authentication plugin rejecting a
-- request), or proxied back from an Service's response body.
--
-- Unlike `kong.service.response`, this module allows mutating the response
-- before sending it back to the client.
--
-- @module kong.response
local cjson = require "cjson.safe"
local checks = require "kong.pdk.private.checks"
local phase_checker = require "kong.pdk.private.phases"
local utils = require "kong.tools.utils"
local ngx = ngx
local arg = ngx.arg
local fmt = string.format
local type = type
local find = string.find
local lower = string.lower
local error = error
local pairs = pairs
local ipairs = ipairs
local concat = table.concat
local tonumber = tonumber
local coroutine = coroutine
local normalize_header = checks.normalize_header
local normalize_multi_header = checks.normalize_multi_header
local validate_header = checks.validate_header
local validate_headers = checks.validate_headers
local check_phase = phase_checker.check
local split = utils.split
local add_header
if ngx and ngx.config.subsystem == "http" then
add_header = require("ngx.resp").add_header
end
local PHASES = phase_checker.phases
local header_body_log = phase_checker.new(PHASES.response,
PHASES.header_filter,
PHASES.body_filter,
PHASES.log,
PHASES.error,
PHASES.admin_api)
local rewrite_access_header = phase_checker.new(PHASES.rewrite,
PHASES.access,
PHASES.response,
PHASES.header_filter,
PHASES.error,
PHASES.admin_api)
local function new(self, major_version)
local _RESPONSE = {}
local MIN_HEADERS = 1
local MAX_HEADERS_DEFAULT = 100
local MAX_HEADERS = 1000
local MIN_STATUS_CODE = 100
local MAX_STATUS_CODE = 599
local MIN_ERR_STATUS_CODE = 400
local GRPC_STATUS_UNKNOWN = 2
local GRPC_STATUS_NAME = "grpc-status"
local GRPC_MESSAGE_NAME = "grpc-message"
local CONTENT_LENGTH_NAME = "Content-Length"
local CONTENT_TYPE_NAME = "Content-Type"
local CONTENT_TYPE_JSON = "application/json; charset=utf-8"
local CONTENT_TYPE_GRPC = "application/grpc"
local ACCEPT_NAME = "Accept"
local HTTP_TO_GRPC_STATUS = {
[200] = 0,
[400] = 3,
[401] = 16,
[403] = 7,
[404] = 5,
[409] = 6,
[429] = 8,
[499] = 1,
[500] = 13,
[501] = 12,
[503] = 14,
[504] = 4,
}
local GRPC_MESSAGES = {
[0] = "OK",
[1] = "Canceled",
[2] = "Unknown",
[3] = "InvalidArgument",
[4] = "DeadlineExceeded",
[5] = "NotFound",
[6] = "AlreadyExists",
[7] = "PermissionDenied",
[8] = "ResourceExhausted",
[9] = "FailedPrecondition",
[10] = "Aborted",
[11] = "OutOfRange",
[12] = "Unimplemented",
[13] = "Internal",
[14] = "Unavailable",
[15] = "DataLoss",
[16] = "Unauthenticated",
}
local HTTP_MESSAGES = {
s400 = "Bad request",
s401 = "Unauthorized",
s402 = "Payment required",
s403 = "Forbidden",
s404 = "Not found",
s405 = "Method not allowed",
s406 = "Not acceptable",
s407 = "Proxy authentication required",
s408 = "Request timeout",
s409 = "Conflict",
s410 = "Gone",
s411 = "Length required",
s412 = "Precondition failed",
s413 = "Payload too large",
s414 = "URI too long",
s415 = "Unsupported media type",
s416 = "Range not satisfiable",
s417 = "Expectation failed",
s418 = "I'm a teapot",
s421 = "Misdirected request",
s422 = "Unprocessable entity",
s423 = "Locked",
s424 = "Failed dependency",
s425 = "Too early",
s426 = "Upgrade required",
s428 = "Precondition required",
s429 = "Too many requests",
s431 = "Request header fields too large",
s451 = "Unavailable for legal reasons",
s494 = "Request header or cookie too large",
s500 = "An unexpected error occurred",
s501 = "Not implemented",
s502 = "An invalid response was received from the upstream server",
s503 = "The upstream server is currently unavailable",
s504 = "The upstream server is timing out",
s505 = "HTTP version not supported",
s506 = "Variant also negotiates",
s507 = "Insufficient storage",
s508 = "Loop detected",
s510 = "Not extended",
s511 = "Network authentication required",
default = "The upstream server responded with %d"
}
---
-- Returns the HTTP status code currently set for the downstream response (as
-- a Lua number).
--
-- If the request was proxied (as per `kong.response.get_source()`), the
-- return value is the response from the Service (identical to
-- `kong.service.response.get_status()`).
--
-- If the request was _not_ proxied and the response was produced by Kong
-- itself (i.e. via `kong.response.exit()`), the return value is
-- returned as-is.
--
-- @function kong.response.get_status
-- @phases header_filter, response, body_filter, log, admin_api
-- @treturn number status The HTTP status code currently set for the
-- downstream response.
-- @usage
-- kong.response.get_status() -- 200
function _RESPONSE.get_status()
check_phase(header_body_log)
return ngx.status
end
---
-- Returns the value of the specified response header, as would be seen by
-- the client once received.
--
-- The list of headers returned by this function can consist of both response
-- headers from the proxied Service _and_ headers added by Kong (e.g. via
-- `kong.response.add_header()`).
--
-- The return value is either a `string`, or can be `nil` if a header with
-- `name` is not found in the response. If a header with the same name is
-- present multiple times in the request, this function returns the value
-- of the first occurrence of this header.
--
-- @function kong.response.get_header
-- @phases header_filter, response, body_filter, log, admin_api
-- @tparam string name The name of the header.
--
-- Header names are case-insensitive and dashes (`-`) can be written as
-- underscores (`_`). For example, the header `X-Custom-Header` can also be
-- retrieved as `x_custom_header`.
--
-- @treturn string|nil The value of the header.
-- @usage
-- -- Given a response with the following headers:
-- -- X-Custom-Header: bla
-- -- X-Another: foo bar
-- -- X-Another: baz
--
-- kong.response.get_header("x-custom-header") -- "bla"
-- kong.response.get_header("X-Another") -- "foo bar"
-- kong.response.get_header("X-None") -- nil
function _RESPONSE.get_header(name)
check_phase(header_body_log)
if type(name) ~= "string" then
error("header name must be a string", 2)
end
local header_value = _RESPONSE.get_headers()[name]
if type(header_value) == "table" then
return header_value[1]
end
return header_value
end
---
-- Returns a Lua table holding the response headers. Keys are header names.
-- Values are either a string with the header value, or an array of strings
-- if a header was sent multiple times. Header names in this table are
-- case-insensitive and are normalized to lowercase, and dashes (`-`) can be
-- written as underscores (`_`). For example, the header `X-Custom-Header` can
-- also be retrieved as `x_custom_header`.
--
-- A response initially has no headers. Headers are added when a plugin
-- short-circuits the proxying by producing a header
-- (e.g. an authentication plugin rejecting a request), or if the request has
-- been proxied, and one of the latter execution phases is currently running.
--
-- Unlike `kong.service.response.get_headers()`, this function returns *all*
-- headers as the client would see them upon reception, including headers
-- added by Kong itself.
--
-- By default, this function returns up to **100** headers. The optional
-- `max_headers` argument can be specified to customize this limit, but must
-- be greater than **1** and equal to or less than **1000**.
--
-- @function kong.response.get_headers
-- @phases header_filter, response, body_filter, log, admin_api
-- @tparam[opt] number max_headers Limits the number of headers parsed.
-- @treturn table headers A table representation of the headers in the
-- response.
--
-- @treturn string err If more headers than `max_headers` were present,
-- returns a string with the error `"truncated"`.
-- @usage
-- -- Given an response from the Service with the following headers:
-- -- X-Custom-Header: bla
-- -- X-Another: foo bar
-- -- X-Another: baz
--
-- local headers = kong.response.get_headers()
--
-- headers.x_custom_header -- "bla"
-- headers.x_another[1] -- "foo bar"
-- headers["X-Another"][2] -- "baz"
function _RESPONSE.get_headers(max_headers)
check_phase(header_body_log)
if max_headers == nil then
return ngx.resp.get_headers(MAX_HEADERS_DEFAULT)
end
if type(max_headers) ~= "number" then
error("max_headers must be a number", 2)
elseif max_headers < MIN_HEADERS then
error("max_headers must be >= " .. MIN_HEADERS, 2)
elseif max_headers > MAX_HEADERS then
error("max_headers must be <= " .. MAX_HEADERS, 2)
end
return ngx.resp.get_headers(max_headers)
end
---
-- This function helps determine where the current response originated
-- from. Since Kong is a reverse proxy, it can short-circuit a request and
-- produce a response of its own, or the response can come from the proxied
-- Service.
--
-- Returns a string with three possible values:
--
-- * `"exit"` is returned when, at some point during the processing of the
-- request, there has been a call to `kong.response.exit()`. This happens
-- when the request was short-circuited by a plugin or by Kong
-- itself (e.g. invalid credentials).
-- * `"error"` is returned when an error has happened while processing the
-- request. For example, a timeout while connecting to the upstream
-- service.
-- * `"service"` is returned when the response was originated by successfully
-- contacting the proxied Service.
--
-- @function kong.response.get_source
-- @phases header_filter, response, body_filter, log, admin_api
-- @treturn string The source.
-- @usage
-- if kong.response.get_source() == "service" then
-- kong.log("The response comes from the Service")
-- elseif kong.response.get_source() == "error" then
-- kong.log("There was an error while processing the request")
-- elseif kong.response.get_source() == "exit" then
-- kong.log("There was an early exit while processing the request")
-- end
function _RESPONSE.get_source(ctx)
if ctx == nil then
check_phase(header_body_log)
ctx = ngx.ctx
end
if ctx.KONG_UNEXPECTED then
return "error"
end
if ctx.KONG_EXITED then
return "exit"
end
if ctx.KONG_PROXIED then
return "service"
end
return "error"
end
---
-- Allows changing the downstream response HTTP status code before sending it
-- to the client.
--
-- @function kong.response.set_status
-- @phases rewrite, access, header_filter, response, admin_api
-- @tparam number status The new status.
-- @return Nothing; throws an error on invalid input.
-- @usage
-- kong.response.set_status(404)
function _RESPONSE.set_status(status)
check_phase(rewrite_access_header)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
if type(status) ~= "number" then
error("code must be a number", 2)
elseif status < MIN_STATUS_CODE or status > MAX_STATUS_CODE then
error(fmt("code must be a number between %u and %u", MIN_STATUS_CODE, MAX_STATUS_CODE), 2)
end
if ngx.headers_sent then
error("headers have already been sent", 2)
end
ngx.status = status
end
---
-- Sets a response header with the given value. This function overrides any
-- existing header with the same name.
--
-- Note: Underscores in header names are automatically transformed into dashes
-- by default. If you want to deactivate this behavior, set the
-- `lua_transform_underscores_in_response_headers` Nginx config option to `off`.
--
-- This setting can be set in the Kong Config file:
--
-- nginx_http_lua_transform_underscores_in_response_headers = off
--
-- Be aware that changing this setting might break any plugins that
-- rely on the automatic underscore conversion.
-- You cannot set Transfer-Encoding header with this function. It will be ignored.
--
-- @function kong.response.set_header
-- @phases rewrite, access, header_filter, response, admin_api
-- @tparam string name The name of the header
-- @tparam string|number|boolean value The new value for the header.
-- @return Nothing; throws an error on invalid input.
-- @usage
-- kong.response.set_header("X-Foo", "value")
function _RESPONSE.set_header(name, value)
check_phase(rewrite_access_header)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
validate_header(name, value)
local lower_name = lower(name)
if lower_name == "transfer-encoding" or lower_name == "transfer_encoding" then
self.log.warn("manually setting Transfer-Encoding. Ignored.")
return
end
ngx.header[name] = normalize_header(value)
end
---
-- Adds a response header with the given value. Unlike
-- `kong.response.set_header()`, this function does not remove any existing
-- header with the same name. Instead, another header with the same name is
-- added to the response. If no header with this name already exists on
-- the response, then it is added with the given value, similarly to
-- `kong.response.set_header().`
--
-- @function kong.response.add_header
-- @phases rewrite, access, header_filter, response, admin_api
-- @tparam string name The header name.
-- @tparam string|number|boolean value The header value.
-- @return Nothing; throws an error on invalid input.
-- @usage
-- kong.response.add_header("Cache-Control", "no-cache")
-- kong.response.add_header("Cache-Control", "no-store")
function _RESPONSE.add_header(name, value)
-- stream subsystem would been stopped by the phase checker below
-- therefore the nil reference to add_header will never have chance
-- to show
check_phase(rewrite_access_header)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
validate_header(name, value)
add_header(name, normalize_header(value))
end
---
-- Removes all occurrences of the specified header in the response sent to
-- the client.
--
-- @function kong.response.clear_header
-- @phases rewrite, access, header_filter, response, admin_api
-- @tparam string name The name of the header to be cleared
-- @return Nothing; throws an error on invalid input.
-- @usage
-- kong.response.set_header("X-Foo", "foo")
-- kong.response.add_header("X-Foo", "bar")
--
-- kong.response.clear_header("X-Foo")
-- -- from here onwards, no X-Foo headers will exist in the response
function _RESPONSE.clear_header(name)
check_phase(rewrite_access_header)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
if type(name) ~= "string" then
error("header name must be a string", 2)
end
ngx.header[name] = nil
end
---
-- Sets the headers for the response. Unlike `kong.response.set_header()`,
-- the `headers` argument must be a table in which each key is a string
-- corresponding to a header's name, and each value is a string, or an
-- array of strings.
--
-- The resulting headers are produced in lexicographical order. The order of
-- entries with the same name (when values are given as an array) is
-- retained.
--
-- This function overrides any existing header bearing the same name as those
-- specified in the `headers` argument. Other headers remain unchanged.
--
-- You cannot set Transfer-Encoding header with this function. It will be ignored.
--
-- @function kong.response.set_headers
-- @phases rewrite, access, header_filter, response, admin_api
-- @tparam table headers
-- @return Nothing; throws an error on invalid input.
-- @usage
-- kong.response.set_headers({
-- ["Bla"] = "boo",
-- ["X-Foo"] = "foo3",
-- ["Cache-Control"] = { "no-store", "no-cache" }
-- })
--
-- -- Will add the following headers to the response, in this order:
-- -- X-Bar: bar1
-- -- Bla: boo
-- -- Cache-Control: no-store
-- -- Cache-Control: no-cache
-- -- X-Foo: foo3
function _RESPONSE.set_headers(headers)
check_phase(rewrite_access_header)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
validate_headers(headers)
for name, value in pairs(headers) do
local lower_name = lower(name)
if lower_name == "transfer-encoding" or lower_name == "transfer_encoding" then
self.log.warn("manually setting Transfer-Encoding. Ignored.")
else
ngx.header[name] = normalize_multi_header(value)
end
end
end
---
-- Returns the full body when the last chunk has been read.
--
-- Calling this function starts buffering the body in
-- an internal request context variable, and sets the current
-- chunk (`ngx.arg[1]`) to `nil` when the chunk is not the
-- last one. When it reads the last chunk, the function returns the full
-- buffered body.
--
-- @function kong.response.get_raw_body
-- @phases `body_filter`
-- @treturn string body The full body when the last chunk has been read,
-- otherwise returns `nil`.
-- @usage
-- local body = kong.response.get_raw_body()
-- if body then
-- body = transform(body)
-- kong.response.set_raw_body(body)
-- end
function _RESPONSE.get_raw_body()
check_phase(PHASES.body_filter)
local body_buffer
local chunk = arg[1]
local eof = arg[2]
if eof then
body_buffer = ngx.ctx.KONG_BODY_BUFFER
if not body_buffer then
return chunk
end
end
if type(chunk) == "string" and chunk ~= "" then
if not eof then
body_buffer = ngx.ctx.KONG_BODY_BUFFER
end
if body_buffer then
local n = body_buffer.n + 1
body_buffer.n = n
body_buffer[n] = chunk
else
body_buffer = {
chunk,
n = 1,
}
ngx.ctx.KONG_BODY_BUFFER = body_buffer
end
end
if eof then
if body_buffer then
body_buffer = concat(body_buffer, "", 1, body_buffer.n)
else
body_buffer = ""
end
arg[1] = body_buffer
return body_buffer
end
arg[1] = nil
return nil
end
---
-- Sets the body of the response.
--
-- The `body` argument must be a string and is not processed in any way.
-- This function can't change the `Content-Length` header if one was
-- added. If you decide to use this function, the `Content-Length` header
-- should also be cleared, for example in the `header_filter` phase.
--
-- @function kong.response.set_raw_body
-- @phases `body_filter`
-- @tparam string body The raw body.
-- @return Nothing; throws an error on invalid inputs.
-- @usage
-- kong.response.set_raw_body("Hello, world!")
-- -- or
-- local body = kong.response.get_raw_body()
-- if body then
-- body = transform(body)
-- kong.response.set_raw_body(body)
-- end
function _RESPONSE.set_raw_body(body)
check_phase(PHASES.body_filter)
if type(body) ~= "string" then
error("body must be a string", 2)
end
if body == "" then -- Needed by Nginx
arg[1] = "\n"
else
arg[1] = body
end
arg[2] = true
ngx.ctx.KONG_BODY_BUFFER = nil
end
local function is_grpc_request()
local req_ctype = ngx.var.content_type
return req_ctype
and find(req_ctype, CONTENT_TYPE_GRPC, 1, true) == 1
and ngx.req.http_version() == 2
end
local function send(status, body, headers)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
ngx.status = status
local has_content_type
local has_content_length
if headers ~= nil then
for name, value in pairs(headers) do
ngx.header[name] = normalize_multi_header(value)
local lower_name = lower(name)
if lower_name == "transfer-encoding" or lower_name == "transfer_encoding" then
self.log.warn("manually setting Transfer-Encoding. Ignored.")
else
ngx.header[name] = normalize_multi_header(value)
end
if not has_content_type or not has_content_length then
if lower_name == "content-type"
or lower_name == "content_type"
then
has_content_type = true
elseif lower_name == "content-length"
or lower_name == "content_length" then
has_content_length = true
end
end
end
end
local res_ctype = ngx.header[CONTENT_TYPE_NAME]
local is_grpc
local is_grpc_output
if res_ctype then
is_grpc = find(res_ctype, CONTENT_TYPE_GRPC, 1, true) == 1
is_grpc_output = is_grpc
else
is_grpc = is_grpc_request()
end
local grpc_status
if is_grpc and not ngx.header[GRPC_STATUS_NAME] then
grpc_status = HTTP_TO_GRPC_STATUS[status]
if not grpc_status then
if status >= 500 and status <= 599 then
grpc_status = HTTP_TO_GRPC_STATUS[500]
elseif status >= 400 and status <= 499 then
grpc_status = HTTP_TO_GRPC_STATUS[400]
elseif status >= 200 and status <= 299 then
grpc_status = HTTP_TO_GRPC_STATUS[200]
else
grpc_status = GRPC_STATUS_UNKNOWN
end
end
ngx.header[GRPC_STATUS_NAME] = grpc_status
end
local json
if type(body) == "table" then
if is_grpc then
if is_grpc_output then
error("table body encoding with gRPC is not supported", 2)
elseif type(body.message) == "string" then
body = body.message
else
self.log.warn("body was removed because table body encoding with " ..
"gRPC is not supported")
body = nil
end
else
local err
json, err = cjson.encode(body)
if err then
error(fmt("body encoding failed while flushing response: %s", err), 2)
end
end
end
local ctx = ngx.ctx
local is_header_filter_phase = ctx.KONG_PHASE == PHASES.header_filter
if json ~= nil then
if not has_content_type then
ngx.header[CONTENT_TYPE_NAME] = CONTENT_TYPE_JSON
end
if not has_content_length then
ngx.header[CONTENT_LENGTH_NAME] = #json
end
if is_header_filter_phase then
ngx.ctx.response_body = json
else
ngx.print(json)
end
elseif body ~= nil then
if is_grpc and not is_grpc_output then
ngx.header[CONTENT_LENGTH_NAME] = 0
ngx.header[GRPC_MESSAGE_NAME] = body
if is_header_filter_phase then
ctx.response_body = ""
else
ngx.print() -- avoid default content
end
else
if not has_content_length then
ngx.header[CONTENT_LENGTH_NAME] = #body
end
if grpc_status and not ngx.header[GRPC_MESSAGE_NAME] then
ngx.header[GRPC_MESSAGE_NAME] = GRPC_MESSAGES[grpc_status]
end
if is_header_filter_phase then
ctx.response_body = body
else
ngx.print(body)
end
end
else
if not has_content_length then
ngx.header[CONTENT_LENGTH_NAME] = 0
end
if grpc_status and not ngx.header[GRPC_MESSAGE_NAME] then
ngx.header[GRPC_MESSAGE_NAME] = GRPC_MESSAGES[grpc_status]
end
if is_grpc then
if is_header_filter_phase then
ctx.response_body = ""
else
ngx.print() -- avoid default content
end
end
end
if is_header_filter_phase then
return ngx.exit(ngx.OK)
end
return ngx.exit(status)
end
local function flush(ctx)
ctx = ctx or ngx.ctx
local response = ctx.delayed_response
return send(response.status_code, response.content, response.headers)
end
local function send_stream(status, body, headers)
if body then
if status < 400 then
-- only sends body to the client for < 400 status code
local res, err = ngx.print(body)
if not res then
error("unable to send body to client: " .. err, 2)
end
else
self.log.err("unable to proxy stream connection, " ..
"status: " .. status .. ", err: ", body)
end
end
return ngx.exit(status)
end
local function flush_stream(ctx)
ctx = ctx or ngx.ctx
local response = ctx.delayed_response
return send_stream(response.status_code, response.content, response.headers)
end
if ngx and ngx.config.subsystem == 'http' then
---
-- This function interrupts the current processing and produces a response.
-- It is typical to see plugins using it to produce a response before Kong
-- has a chance to proxy the request (e.g. an authentication plugin rejecting
-- a request, or a caching plugin serving a cached response).
--
-- It is recommended to use this function in conjunction with the `return`
-- operator, to better reflect its meaning:
--
-- ```lua
-- return kong.response.exit(200, "Success")
-- ```
--
-- Calling `kong.response.exit()` interrupts the execution flow of
-- plugins in the current phase. Subsequent phases will still be invoked.
-- For example, if a plugin calls `kong.response.exit()` in the `access`
-- phase, no other plugin is executed in that phase, but the
-- `header_filter`, `body_filter`, and `log` phases are still executed,
-- along with their plugins. Plugins should be programmed defensively
-- against cases when a request is **not** proxied to the Service, but
-- instead is produced by Kong itself.
--
-- 1. The first argument `status` sets the status code of the response that
-- is seen by the client.
--
-- In L4 proxy mode, the `status` code provided is primarily for logging
-- and statistical purposes, and is not visible to the client directly.
-- In this mode, only the following status codes are supported:
--
-- * 200 - OK
-- * 400 - Bad request
-- * 403 - Forbidden
-- * 500 - Internal server error
-- * 502 - Bad gateway
-- * 503 - Service unavailable
--
-- 2. The second, optional, `body` argument sets the response body. If it is
-- a string, no special processing is done, and the body is sent
-- as-is. It is the caller's responsibility to set the appropriate
-- `Content-Type` header via the third argument.
--
-- As a convenience, `body` can be specified as a table. In that case,
-- the `body` is JSON-encoded and has the `application/json` Content-Type
-- header set.
--
-- On gRPC, we cannot send the `body` with this function, so
-- it sends `"body"` in the `grpc-message` header instead.
-- * If the body is a table, it looks for the `message` field in the body,
-- and uses that as a `grpc-message` header.
-- * If you specify `application/grpc` in the `Content-Type` header, the
-- body is sent without needing the `grpc-message` header.
--
-- In L4 proxy mode, `body` can only be `nil` or a string. Automatic JSON
-- encoding is not available. When `body` is provided, depending on the
-- value of `status`, the following happens:
--
-- * When `status` is 500, 502 or 503, then `body` is logged in the Kong
-- error log file.
-- * When the `status` is anything else, `body` is sent back to the L4 client.
--
-- 3. The third, optional, `headers` argument can be a table specifying
-- response headers to send. If specified, its behavior is similar to
-- `kong.response.set_headers()`. This argument is ignored in L4 proxy mode.
--
-- Unless manually specified, this method automatically sets the
-- `Content-Length` header in the produced response for convenience.
-- @function kong.response.exit
-- @phases preread, rewrite, access, admin_api, header_filter (only if `body` is nil)
-- @tparam number status The status to be used.
-- @tparam[opt] table|string body The body to be used.
-- @tparam[opt] table headers The headers to be used.
-- @return Nothing; throws an error on invalid input.
-- @usage
-- return kong.response.exit(403, "Access Forbidden", {
-- ["Content-Type"] = "text/plain",
-- ["WWW-Authenticate"] = "Basic"
-- })
--
-- ---
--
-- return kong.response.exit(403, [[{"message":"Access Forbidden"}]], {
-- ["Content-Type"] = "application/json",
-- ["WWW-Authenticate"] = "Basic"
-- })
--
-- ---
--
-- return kong.response.exit(403, { message = "Access Forbidden" }, {
-- ["WWW-Authenticate"] = "Basic"
-- })
--
-- ---
--
-- -- In L4 proxy mode
-- return kong.response.exit(200, "Success")
--
function _RESPONSE.exit(status, body, headers)
if self.worker_events and ngx.get_phase() == "content" then
self.worker_events.poll()
end
check_phase(rewrite_access_header)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
if type(status) ~= "number" then
error("code must be a number", 2)
elseif status < MIN_STATUS_CODE or status > MAX_STATUS_CODE then
error(fmt("code must be a number between %u and %u", MIN_STATUS_CODE, MAX_STATUS_CODE), 2)
end
if body ~= nil and type(body) ~= "string" and type(body) ~= "table" then
error("body must be a nil, string or table", 2)
end
if headers ~= nil and type(headers) ~= "table" then
error("headers must be a nil or table", 2)
end
if headers ~= nil then
validate_headers(headers)
end
local ctx = ngx.ctx
ctx.KONG_EXITED = true
if ctx.delay_response and not ctx.delayed_response then
ctx.delayed_response = {
status_code = status,
content = body,
headers = headers,
}
ctx.delayed_response_callback = flush
coroutine.yield()
else
return send(status, body, headers)
end
end
else
local VALID_CODES = {
[200] = true,
[400] = true,
[403] = true,
[500] = true,
[502] = true,
[503] = true,
-- NOTE: when adding new code, change the documentation and error
-- message raised below accordingly
--
-- Code are from http://lxr.nginx.org/source/src/stream/ngx_stream.h#0029
}
function _RESPONSE.exit(status, body, headers)
if type(status) ~= "number" then
error("code must be a number", 2)
elseif not VALID_CODES[status] then
error("unacceptable code, only 200, 400, 403, 500, 502 and 503 " ..
"are accepted", 2)
end
if body ~= nil then
if type(body) == "table" then
local err
body, err = cjson.encode(body)
if err then
error("invalid body: " .. err, 2)
end
end
if type(body) ~= "string" then
error("body must be a nil, string or table", 2)
end
end
local ctx = ngx.ctx
ctx.KONG_EXITED = true
if ctx.delay_response and not ctx.delayed_response then
ctx.delayed_response = {
status_code = status,
content = body,
headers = headers,
}
ctx.delayed_response_callback = flush_stream
coroutine.yield()
else
return send_stream(status, body, headers)
end
end
end
local function get_response_type(content_header)
local type = CONTENT_TYPE_JSON
if content_header ~= nil then
local accept_values = split(content_header, ",")
local max_quality = 0
for _, value in ipairs(accept_values) do
local mimetype_values = split(value, ";")
local name
local quality = 1
for _, entry in ipairs(mimetype_values) do
local m = ngx.re.match(entry, [[^\s*(\S+\/\S+)\s*$]], "ajo")
if m then
name = m[1]
else
m = ngx.re.match(entry, [[^\s*q=([0-9]*[\.][0-9]+)\s*$]], "ajoi")
if m then
quality = tonumber(m[1])
end
end
end
if name and quality > max_quality then
type = utils.get_mime_type(name)
max_quality = quality
end
end
end
return type
end
---
-- This function interrupts the current processing and produces an error
-- response.
--
-- It is recommended to use this function in conjunction with the `return`
-- operator, to better reflect its meaning:
--
-- ```lua
-- return kong.response.error(500, "Error", {["Content-Type"] = "text/html"})
-- ```
--
-- 1. The `status` argument sets the status code of the response that
-- is seen by the client. The status code must an error code, that is,
-- greater than 399.
--
-- 2. The optional `message` argument sets the message describing
-- the error, which is written in the body.
--
-- 3. The optional `headers` argument can be a table specifying response
-- headers to send. If specified, its behavior is similar to
-- `kong.response.set_headers()`.
--
-- This method sends the response formatted in JSON, XML, HTML or plaintext.
-- The actual format is determined using one of the following options, in
-- this order:
-- - Manually specified in the `headers` argument using the `Content-Type`
-- header.
-- - Conforming to the `Accept` header from the request.
-- - If there is no setting in the `Content-Type` or `Accept` header, the
-- response defaults to JSON format. Also see the `Content-Length`
-- header in the produced response for convenience.
-- @function kong.response.error
-- @phases rewrite, access, admin_api, header_filter (only if `body` is nil)
-- @tparam number status The status to be used (>399).
-- @tparam[opt] string message The error message to be used.
-- @tparam[opt] table headers The headers to be used.
-- @return Nothing; throws an error on invalid input.
-- @usage
-- return kong.response.error(403, "Access Forbidden", {
-- ["Content-Type"] = "text/plain",
-- ["WWW-Authenticate"] = "Basic"
-- })
--
-- ---
--
-- return kong.response.error(403, "Access Forbidden")
--
-- ---
--
-- return kong.response.error(403)
function _RESPONSE.error(status, message, headers)
if self.worker_events and ngx.get_phase() == "content" then
self.worker_events.poll()
end
check_phase(rewrite_access_header)
if ngx.headers_sent then
error("headers have already been sent", 2)
end
if type(status) ~= "number" then
error("code must be a number", 2)
elseif status < MIN_ERR_STATUS_CODE or status > MAX_STATUS_CODE then
error(fmt("code must be a number between %u and %u", MIN_ERR_STATUS_CODE,
MAX_STATUS_CODE), 2)
end
if message ~= nil then
if type(message) == "table" then
local err
message, err = cjson.encode(message)
if err then
error("could not JSON encode the error message: " .. err, 2)
end
end
if type(message) ~= "string" then
error("message must be a nil, a string or a table", 2)
end
end
if headers ~= nil and type(headers) ~= "table" then
error("headers must be a nil or table", 2)
end
if headers ~= nil then
validate_headers(headers)
else
headers = {}
end
local content_type_header = headers[CONTENT_TYPE_NAME]
local content_type = content_type_header and content_type_header[1]
or content_type_header
if content_type_header == nil then
if is_grpc_request() then
content_type = CONTENT_TYPE_GRPC
else
content_type_header = ngx.req.get_headers()[ACCEPT_NAME]
if type(content_type_header) == "table" then
content_type_header = content_type_header[1]
end
content_type = get_response_type(content_type_header)
end
end
headers[CONTENT_TYPE_NAME] = content_type
local body
if content_type ~= CONTENT_TYPE_GRPC then
local actual_message = message or
HTTP_MESSAGES["s" .. status] or
fmt(HTTP_MESSAGES.default, status)
body = fmt(utils.get_error_template(content_type), actual_message)
end
local ctx = ngx.ctx
ctx.KONG_EXITED = true
if ctx.delay_response and not ctx.delayed_response then
ctx.delayed_response = {
status_code = status,
content = body,
headers = headers,
}
ctx.delayed_response_callback = flush
coroutine.yield()
else
return send(status, body, headers)
end
end
return _RESPONSE
end
return {
new = new,
}