17 Why Being Able to Handle Dynamic Requests and Responses Better Is Key to a Web Server

17 Why Being Able to Handle Dynamic Requests and Responses Better Is Key to a Web Server #

Hello, I’m Wen Ming. After the introduction in the previous content, I believe you already have a basic understanding of the concept of OpenResty and how to learn it. In today’s lesson, let’s take a look at how OpenResty handles terminal requests and responses.

Although OpenResty is based on NGINX web server, it is fundamentally different from NGINX: NGINX is driven by static configuration files, while OpenResty is driven by Lua API, which provides more flexibility and programmability.

Now, let me show you the benefits brought by Lua API.

API Categories #

First of all, we need to know that the API of OpenResty can be mainly classified into the following categories:

  • Handling requests and responses;
  • SSL-related;
  • shared dict;
  • cosocket;
  • Handling Layer 4 traffic;
  • process and worker;
  • Retrieving NGINX variables and configurations;
  • String, time, encoding and decoding, and other common functions.

Here, I suggest you open the Lua API documentation of OpenResty and compare it with the API list to see if you can relate it to this classification.

The API of OpenResty not only exists in the lua-nginx-module project but also in the lua-resty-core project. For example, these APIs: ngx.ssl, ngx.base64, ngx.errlog, ngx.process, ngx.re.split, ngx.resp.add_header, ngx.balancer, ngx.semaphore, ngx.ocsp.

As for APIs that are not in the lua-nginx-module project, you need to require them separately in order to use them. For example, if you want to use the split function to split a string, you need to call it like this:

$ 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, this may bring you some confusion: in the lua-nginx-module project, there are several ngx.re APIs like ngx.re.sub, ngx.re.find, but why is it that only ngx.re.split API needs to be required before using it?

In fact, as we mentioned earlier in the lua-resty-core section, OpenResty’s new APIs are implemented in the lua-resty-core repository through FFI. Therefore, there may be such a sense of disconnection. Naturally, I also look forward to the lua-nginx-module and lua-resty-core projects merging in the future to completely solve such issues.

Request #

Next, let’s take a closer look at how OpenResty handles terminal requests and responses. First, let’s take a look at the APIs that handle requests. However, there are more than 20 APIs starting with ngx.req, so where do we start?

We know that an HTTP request message consists of three parts: the request line, the request headers, and the request body. Therefore, I will introduce the APIs according to these three parts.

Request Line #

First is the request line, which contains the request method, URI, and HTTP protocol version. In NGINX, you can use built-in variables to obtain these values. In OpenResty, the equivalent is the ngx.var.* API. Let’s look at two examples.

  • $scheme is an NGINX built-in variable that represents the name of the protocol, which can be “http” or “https”. In OpenResty, you can obtain the same value by using ngx.var.scheme.
  • $request_method represents the request method, such as “GET” or “POST”. In OpenResty, you can obtain the same value by using ngx.var.request_method.

For a complete list of NGINX built-in variables, you can visit the NGINX official documentation: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.

So why does OpenResty specifically provide APIs for handling the request line, even though you can obtain the data in the request line using ngx.var.*?

This is the result of considering multiple factors:

  • First, the consideration of performance. ngx.var is not efficient, so it is not recommended to read it repeatedly.
  • There is also consideration for program friendliness. ngx.var returns a string, not a Lua object, which makes it difficult to handle situations where multiple values may be returned, such as obtaining args.
  • Additionally, there is consideration for flexibility. Most ngx.var variables are read-only, but there are a few variables that can be modified, such as $args and limit_rate. However, there are many cases where we need to modify the method, URI, and args.

Therefore, OpenResty provides several APIs specifically for manipulating the request line, which can be used to rewrite the request line for subsequent redirection and other operations.

First, let’s look at how to use an API to obtain the HTTP protocol version number. OpenResty’s ngx.req.http_version API and NGINX’s $server_protocol variable have the same effect, which is to return the HTTP protocol version number. However, the return value of this API is in numeric format, not a string. The possible values are 2.0, 1.0, 1.1, and 0.9. If the result is not within these values, nil will be returned.

Next, let’s see how to obtain the request method in the request line. As mentioned earlier, ngx.req.get_method and NGINX’s $request_method variable have the same effect and return the name of the method in string format.

However, the API for rewriting the current HTTP request method, ngx.req.set_method, does not accept a string as its parameter. Instead, it accepts built-in numeric constants. For example, the following code changes the request method to POST:

ngx.req.set_method(ngx.HTTP_POST)

To verify that ngx.HTTP_POST is indeed a number rather than a string, you can print its value and check if the output is 8:

$ resty -e 'print(ngx.HTTP_POST)'

With this difference, it is easy to make mistakes when writing code. If the confusion only happens when setting the value, it is okay because the API will crash and report a 500 error. However, if it happens in a logic block like this:

if (ngx.req.get_method() == ngx.HTTP_POST) then
    -- do something
end

This code will run normally and won’t report any errors. It may even be difficult to detect during code review. Unfortunately, I have made a similar mistake before, and I still remember it vividly: at that time, it had already gone through two rounds of code review and incomplete test cases, but in the end, we only discovered it because of an anomaly in the production environment.

In this kind of situation, besides being more careful and adding additional layers of abstraction, there are no effective methods to solve it. When designing your own business API, you can also consider these aspects more and try to keep the formats of the get and set methods consistent, even though it may sacrifice some performance.

In addition, in the methods for rewriting the request line, there are two APIs: ngx.req.set_uri and ngx.req.set_uri_args, which can be used to rewrite the URI and args. Let’s take a look at this NGINX configuration:

rewrite ^ /foo?a=3? break;

So, how can we achieve the same effect using equivalent Lua APIs? The answer is the two lines of code below.

ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")

Actually, 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 set to true, it is equivalent to setting the flag of the rewrite directive to last, instead of break as shown in the above example.

However, personally I don’t like the configuration of the flag for the rewrite directive. I find it difficult to understand and remember, and it is far less intuitive and maintainable than using code.

Request Headers #

Let’s take a look at the APIs related to request headers. We know that HTTP request headers are in the format of key: value, 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 retrieve request headers, and the return type is a 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

By default, it returns the first 100 headers. If there are more than 100 headers, it will return the error message truncated, and it is up to the developer to decide how to handle it. You might be curious why this is the case, but I’ll leave that as a mystery for now. I will mention it in the chapter on security vulnerabilities later.

However, it should be noted that OpenResty does not provide an API to get a specific request header, that is, there is no ngx.req.header['host'] format. If you have such a requirement, you need to use the NGINX variable $http_xxx to achieve it. In OpenResty, it is ngx.var.http_xxx to retrieve headers in this way.

After learning how to get request headers, let’s see how to modify and delete request headers. Both of these operations are quite straightforward:

ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")

Of course, the official documentation also mentions other methods to delete request headers, such as setting the header value to nil, but for the sake of code clarity, I still recommend using clear_header for consistency.

Request Body #

Finally, let’s look at the request body. For performance reasons, OpenResty does not actively read the content of the request body unless you force it by enabling the lua_need_request_body directive in nginx.conf. In addition, for relatively large request bodies, OpenResty will save the content in a temporary file on disk. So, the complete process of reading the request body is as follows:

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 contains blocking IO operations to read the disk file. You should adjust the client_body_buffer_size configuration size (which is 16 KB by default on 64-bit systems) according to the actual situation to minimize blocking operations. You can also set client_body_buffer_size and client_max_body_size to be the same, to completely handle the request body in memory. Of course, this depends on the size of your memory and the number of concurrent requests you are handling.

In addition, the request body can also be modified. The APIs ngx.req.set_body_data and ngx.req.set_body_file accept a string and a local disk file as input parameters, respectively, to rewrite the request body. However, such operations are not common. You can refer to the documentation for more detailed information.

## Response

After processing the request, we need to send a response back to the client. Similar to the request message, the response message is also composed of several parts: the status line, response headers, and the response body. Next, I will introduce the corresponding APIs for each of these three parts.

### Status Line

In the status line, the status code is what we mainly focus on. By default, the returned HTTP status code is 200, which is the built-in constant `ngx.HTTP_OK` in OpenResty. However, in the world of code, the code that handles exceptional cases always occupies the majority.

If you detect that the request message is malicious, you need to terminate the request:

ngx.exit(ngx.HTTP_BAD_REQUEST)


However, in the HTTP status codes of OpenResty, there is a special constant: `ngx.OK`. When you use `ngx.exit(ngx.OK)`, the request will exit the current processing phase and enter the next phase, instead of directly returning to the client.

Of course, you can also choose not to exit but only use `ngx.status` to rewrite the status code, like this:

ngx.status = ngx.HTTP_FORBIDDEN


If you want to learn more about status code constants, you can find them in the [documentation](https://github.com/openresty/lua-nginx-module/#http-status-constants).

### Response Headers

When it comes to response headers, actually, there are two methods you can use to set them. The first is the simplest one:

ngx.header.content_type = ’text/plain' ngx.header[“X-My-Header”] = ‘blah blah’ ngx.header[“X-My-Header”] = nil – delete


Here, `ngx.header` saves the information of the response headers, and you can read, modify, and delete them.

The second method to set response headers is `ngx_resp.add_header`, which is from the lua-resty-core repository. It can add a new header information, and you can call it like this:

local ngx_resp = require “ngx.resp” ngx_resp.add_header(“Foo”, “bar”)


The difference from the first method is that `add_header` will not overwrite an existing field with the same name.

### Response Body

Finally, let's take a look at the response body. In OpenResty, you can use `ngx.say` and `ngx.print` to output the response body:

ngx.say(‘hello, world’)


These two APIs have the same functionality, the only difference is that `ngx.say` adds a newline character at the end.

To avoid inefficient string concatenation, `ngx.say / ngx.print` not only support strings as parameters but also support arrays:

$ resty -e ’ngx.say({“hello”, “, “, “world”})' hello, world


By doing this, the string concatenation is skipped at the Lua level, and the function that is not good at it is handed over to the C function to handle.
## Conclusion

At this point, let's review the content we covered today. We introduced the OpenResty API in relation to HTTP request and response messages. You can see that compared to NGINX directives, the OpenResty API is more flexible and powerful.

So, when you handle HTTP requests, do you feel that the Lua API provided by OpenResty meets your needs? Feel free to leave a comment to discuss this topic, and don't hesitate to share this article with your colleagues and friends. Let's communicate and grow together.