Files
2026-07-13 12:32:21 +08:00

1189 lines
37 KiB
Lua

---
-- 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 buffer = require "string.buffer"
local cjson = require "cjson.safe"
local checks = require "kong.pdk.private.checks"
local phase_checker = require "kong.pdk.private.phases"
local request_id = require "kong.observability.tracing.request_id"
local constants = require "kong.constants"
local tools_http = require "kong.tools.http"
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 coroutine = coroutine
local cjson_encode = cjson.encode
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 add_header
local is_http_subsystem = ngx and ngx.config.subsystem == "http"
if is_http_subsystem then
add_header = require("ngx.resp").add_header
end
local RESPONSE_SOURCE_TYPES = constants.RESPONSE_SOURCE.TYPES
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.balancer,
PHASES.header_filter,
PHASES.error,
PHASES.admin_api)
local function new(self, major_version)
local _RESPONSE = {}
local MIN_HEADERS = 1
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 get_http_error_message
do
local HTTP_ERROR_MESSAGES = {
[400] = "Bad request",
[401] = "Unauthorized",
[402] = "Payment required",
[403] = "Forbidden",
[404] = "Not found",
[405] = "Method not allowed",
[406] = "Not acceptable",
[407] = "Proxy authentication required",
[408] = "Request timeout",
[409] = "Conflict",
[410] = "Gone",
[411] = "Length required",
[412] = "Precondition failed",
[413] = "Payload too large",
[414] = "URI too long",
[415] = "Unsupported media type",
[416] = "Range not satisfiable",
[417] = "Expectation failed",
[418] = "I'm a teapot",
[421] = "Misdirected request",
[422] = "Unprocessable entity",
[423] = "Locked",
[424] = "Failed dependency",
[425] = "Too early",
[426] = "Upgrade required",
[428] = "Precondition required",
[429] = "Too many requests",
[431] = "Request header fields too large",
[451] = "Unavailable for legal reasons",
[494] = "Request header or cookie too large",
[500] = "An unexpected error occurred",
[501] = "Not implemented",
[502] = "An invalid response was received from the upstream server",
[503] = "The upstream server is currently unavailable",
[504] = "The upstream server is timing out",
[505] = "HTTP version not supported",
[506] = "Variant also negotiates",
[507] = "Insufficient storage",
[508] = "Loop detected",
[510] = "Not extended",
[511] = "Network authentication required",
}
function get_http_error_message(status)
local msg = HTTP_ERROR_MESSAGES[status]
if msg then
return msg
end
msg = fmt("The upstream server responded with %d", status)
HTTP_ERROR_MESSAGES[status] = msg
return msg
end
end
---
-- 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 (or what has been
-- configured using `lua_max_resp_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()
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 RESPONSE_SOURCE_TYPES.ERROR
end
if ctx.KONG_EXITED then
return RESPONSE_SOURCE_TYPES.EXIT
end
if ctx.KONG_PROXIED then
return RESPONSE_SOURCE_TYPES.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
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 array of strings|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_multi_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 array of strings|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_multi_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 = ngx.ctx.KONG_BODY_BUFFER
local chunk = arg[1]
local eof = arg[2]
if eof and not body_buffer then
return chunk
end
if type(chunk) == "string" and chunk ~= "" then
if not body_buffer then
body_buffer = buffer.new()
ngx.ctx.KONG_BODY_BUFFER = body_buffer
end
body_buffer:put(chunk)
end
if eof then
if body_buffer then
body_buffer = body_buffer:get()
else
body_buffer = ""
end
arg[1] = body_buffer
ngx.ctx.KONG_BODY_BUFFER = nil
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
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 is_http_subsystem 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
---
-- 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
local accept_header = ngx.req.get_headers()[ACCEPT_NAME]
content_type = tools_http.get_response_type(accept_header)
end
end
headers[CONTENT_TYPE_NAME] = content_type
local body
if content_type ~= CONTENT_TYPE_GRPC then
local actual_message = message or get_http_error_message(status)
local rid = request_id.get() or ""
body = fmt(tools_http.get_error_template(content_type), actual_message, rid)
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,
}