OpenResty Is The Enhanced NGINX With Dynamic Requests and Responses

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

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 either http or https; in OpenResty, you can use ngx.var.scheme to return to the same value.
  • $request_method represents the request method like GET, POST, etc.; in OpenResty, you can return to the same value via ngx.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 getting args, 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 and limit_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.