OpenResty Is The Enhanced NGINX With Dynamic Requests and Responses
API7.ai
October 23, 2022
After the previous introduction, you must understand the concept of OpenResty and how to learn it. This article will guide us on how OpenResty handles client requests and responses.
Although OpenResty is an NGINX-based web server, it is fundamentally different from NGINX: NGINX is driven by static configuration files, while OpenResty is driven by Lua API, offering more flexibility and programmability.
Let me take you through the benefits of the Lua API.
API Categories
First, we need to know that the OpenResty API is divided into the following broad categories.
- Request and response processing.
- SSL-related.
- Shared dict.
- Cosocket.
- Handling four layers of traffic.
- Process and worker.
- Access to NGINX variables and configuration.
- String, time, codec, and other general functions, etc.
Here, I suggest you also open OpenResty's Lua API documentation and check it against the API list to see if you can relate to this category.
OpenResty APIs exist not only in the lua-nginx-module project, but also in the lua-resty-core project, such as ngx.ssl
, ngx.base64
, ngx.errlog
, ngx.process
, ngx.re.split
, ngx.resp.add_header
, ngx.balancer
, ngx.semaphore
, ngx.ocsp
, and other APIs.
For APIs that are not in the lua-nginx-module project, you need to require them separately to use them. For example, if you want to use the split function, you need to call it as follows.
$ resty -e 'local ngx_re = require "ngx.re"
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
print(res)
'
Of course, it may confuse you: in the lua-nginx-module
project, there are several APIs starting with ngx.re.sub
, ngx.re.find
, etc. Why is it that the API ngx.re.split
is the only one that needs to require first before being used?
As we mentioned in the previous lua-resty-core
chapter, the new OpenResty APIs are implemented in the lua-rety-core
repository by way of FFI, so there is inevitably a sense of fragmentation. I'm looking forward to solving this problem in the future by merging the lua-nginx-module
and lua-resty-core
projects.
Request
Next, let's look at how OpenResty handles client requests and responses. First, let's look at the API for handling requests, but there are more than 20 APIs starting with ngx.req
, so how do we start?
We know that HTTP request messages consist of three parts: the request line, the request header, and the request body, so I'll introduce the API in these three parts.
Request Line
The first is the request line, which contains the request method, URI, and HTTP protocol version for HTTP. In NGINX, you can get this value using a built-in variable, while in OpenResty, it corresponds to the ngx.var.*
API. Let's look at two examples.
- The built-in variable
$scheme
, which represents the name of the protocol in NGINX, is eitherhttp
orhttps
; in OpenResty, you can usengx.var.scheme
to return to the same value. $request_method
represents the request method likeGET
,POST
, etc.; in OpenResty, you can return to the same value viangx.var.request_method
.
You can visit the NGINX official documentation to get a complete list of NGINX built-in variables: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.
So the question arises: why does OpenResty provide a separate API for request lines when you can get the data in the request line by returning the value of a variable like ngx.var.*
?
The result contains many factors:
- First of all, it is not recommended to repeatedly read
ngx.var
due to its inefficient performance. - Second, out of consideration of the program-friendly aspect,
ngx.var
returns a string, not a Lua object. It isn't easy to handle when gettingargs
, which may return multiple values. - Third, from the aspect of flexibility, most of
ngx.var
is read-only, and only a few variables are writable, such as$args
andlimit_rate
. However, we often need to modify the method, URI, and args.
Therefore, OpenResty provides several APIs dedicated to manipulating request lines, which can rewrite the request line for subsequent operations such as redirection.
Let's look at how to get the HTTP protocol version number through the API. OpenResty API ngx.req.http_version
does the same thing as NGINX $server_protocol
variable: returning to the version number of HTTP protocol. However, the return value of this API is not a string but in numeric format, the possible values of which are 2.0
, 1.0
, 1.1
, and 0.9
. Nil
is returned if the result is out of the range of these values.
Let's look at the method of getting the request in the request line. As mentioned, the role and return value of ngx.req.get_method
and NGINX's $request_method
variables are the same: in string format.
However, the parameter format of the current HTTP request method ngx.req.set_method
is not a string but built-in numeric constants. For example, the following code rewrites the request method to POST.
ngx.req.set_method(ngx.HTTP_POST)
To verify that the built-in constant, ngx.HTTP_POST
is indeed a number and not a string, you can print out its value and see if the output is 8.
resty -e 'print(ngx.HTTP_POST)'
In this way, the return value of the get
method is a string, while the input value of the set
method is a number. It is OK when the set method passes a confusing value because the API can crash and report with a 500
error. However, in the following judging logic:
if (ngx.req.get_method() == ngx.HTTP_POST) then
-- do something
end
This kind of code works fine, report no errors, and is hard to find even during code reviews. I made a similar mistake before and can still remember it: I had already gone through two rounds of code reviews and incomplete test cases to try to cover it. Ultimately, an online environment anomaly traced me to the problem.
There is no practical way to solve such a problem except to be more careful or to add another layer of encapsulation. When you design your business API, you can also consider and keep the consistent parameter format of get
and set
methods, even if you need to sacrifice some performance.
In addition, among the methods for rewriting request lines, there are two APIs, ngx.req.set_uri
and ngx.req.set_uri_args
, which can be used to rewrite URI and args. Let's look at this NGINX configuration.
rewrite ^ /foo?a=3? break;
So, how can we solve it with the equivalent Lua API? The answer is the following two lines of code.
ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")
If you have read the official documentation, you will find that ngx.req.set_uri
has a second parameter: jump
, which is "false" by default. If you set it as "true", it is equal to setting the flag of the rewrite
command to last
instead of break
in the example above.
However, I'm not too fond of the flag configuration of the rewrite
command as it's unreadable and unrecognizable and far less intuitive and maintainable than code.
Request Header
As we know, HTTP request headers are in the key : value
format, for example:
Accept: text/css,*/*;q=0.1
Accept-Encoding: gzip, deflate, br
In OpenResty, you can use ngx.req.get_headers
to parse and get the request headers, and the return value type is table.
local h, err = ngx.req.get_headers()
if err == "truncated" then
-- one can choose to ignore or reject the current request here
end
for k, v in pairs(h) do
...
end
It is defaulted to return to the first 100 headers. If the number exceeds 100, it will report a truncated
error, leaving it up to the developer to decide how to handle it. You may wonder why to take it this way, which I will mention later in the section on security vulnerabilities.
However, we should note that OpenResty provides no specific API for getting a specified request header, which means no form as ngx.req.header['host']
. If you have such a need, you must rely on the NGINX variable $http_xxx
to achieve it. Then in OpenResty, you can get it by ngx.var.http_xxx
.
Now let's look at how we should rewrite and delete the request header. The APIs for both operations is quite intuitive:
ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")
Of course, the official documentation also mentions other methods to remove the request header, such as setting the value of the title to nil
, etc. However, I still recommend using clear_header
to do it uniformly for code clarity.
Request Body
Finally, let's look at the request body. For performance reasons, OpenResty does not actively read the request body unless you force the lua_need_request_body
directive to be enabled in nginx.conf
. In addition, for larger request bodies, OpenResty saves the contents to a temporary file on disk, so the entire process of reading the request body looks like this.
ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
local tmp_file = ngx.req.get_body_file()
-- io.open(tmp_file)
-- ...
end
This code has an IO-blocking operation to read the disk file. You should adjust the configuration of the client_body_buffer_size
(16 KB by default on 64-bit systems) to minimize the blocking operations; you can also configure client_body_buffer_size
and client_max_body_size
to be the same and handle them entirely in memory, depending on the size of your memory and the number of concurrent requests you take.
In addition, the request body can be rewritten. The two APIs ngx.req.set_body_data
and ngx.req.set_body_file
accept a string and a local disk file as input parameters to accomplish the rewriting of the request body. However, this type of operation is uncommon, and you can check the documentation for more details.
Response
After the request is processed, we need to send a response back to the client. Like the request message, the response message also consists of several parts: the status line, the response header, and the response body. I will introduce the corresponding APIs according to these three parts.
Status Line
The main thing we are concerned about in the status line is the status code. By default, the HTTP status code returned is 200, which is the constant ngx.HTTP_OK
built into OpenResty. But in the world of code, it is always the code that handles the most exceptional cases.
If you detect the request message and find that it is a malicious request, then you need to terminate the request:
ngx.exit(ngx.HTTP_BAD_REQUEST)
However, there is a particular constant in OpenResty's HTTP status code: ngx.OK
. In the situation of ngx.exit(ngx.OK)
, the request exits the current processing phase and moves on to the next stage rather than returning directly to the client.
Of course, you can also choose not to exit and just rewrite the status code using ngx.status
, as written in the following way.
ngx.status = ngx.HTTP_FORBIDDEN
You can look them up in the documentation if you want to know more about the status code constants.
Response Header
Regarding the response header, there are two ways you can set it up. The first one is the simplest.
ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- delete
Here the ngx.header
holds the response header information, which can be read, modified, and deleted.
The second way to set the response header is ngx_resp.add_header
, from the lua-resty-core
repository, which adds a header message, called with:
local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")
The difference with the first method is that add_header
does not overwrite an existing field of the same name.
Response Body
Finally, look at the response body. In OpenResty, you can use ngx.say
and ngx.print
to output the response body.
ngx.say('hello, world')
The functionality of the two APIs is identical, the only difference being that ngx.say
has a line feed at the end.
To avoid the inefficiency of string splicing, ngx.say / ngx.print
supports strings and array formats as parameters.
$ resty -e 'ngx.say({"hello", ", ", "world"})'
hello, world
This method skips the string splicing at the Lua level and leaves it to the C functions to handle.
Summary
Let's review today's content. We introduce the OpenResty APIs associated with the request and response messages. As you can see, the OpenResty API is more flexible and powerful than the NGINX directive.
Consequently, is the Lua API provided by OpenResty enough to meet your needs when you are handling HTTP requests? Please leave your comments and share this article with your colleagues and friends so that we can communicate and improve together.