Getting Started with iRules LX, Part 3: Coding & Exception Handling
So far in this series, we have covered the conceptual overview, components, workflows, licensing and provisioning of iRules LX. In this article, we will actually get our hands wet with some iRules LX code!
TCL Commands
As mentioned in the first article, we still need iRules TCL to make a RPC to Node.js. iRules LX introduces three new iRules TCL commands -
- Creates a RPC handle to the plugin's extension. We will use the variable from this in our other commands.ILX::init
- Does the RPC to the Node.js process to send data and receive the result (2 way communication).ILX::call
- Does an RPC to the Node.js process to send data only (1 way communication, no response).ILX::notify
There is one caveat to the
and ILX::call
commands: you can only send 64KB of data. This includes overhead used to encapsulate the RPC so you will need to allow about 500B-1KB for the overhead. If you need to send more data you will need to create code that will send data over in chunks.ILX::notify
TCL Coding
Here is a TCL template for how we would use iRules LX.
when HTTP_REQUEST { set ilx_handle [ILX::init "my_plgin" "ext1"] if {[catch {ILX::call $ilx_handle "my_method" $user $passwd} result]} { log local0.error "Client - [IP::client_addr], ILX failure: $result" # Send user graceful error message, then exit event return } # If one value is returned, it becomes TCL string if {$result eq 'yes'} { # Some action } # If multiple values are returned, it becomes TCL list set x [ lindex $result 0] set y [ lindex $result 1] }
So as not to overwhelm you, we'll break this down to bite size chunks.
Taking line 2 from above, we will use
command to create our RPC handle to extension ILX::init
of our LX plugin ext1
and store it in the variable my_plugin
. This is why the names of the extensions and plugin matter. ilx_handle
set ilx_handle [ILX::init "my_plgin" "ext1"]
Next, we can take the handle and do the RPC -
if {[catch {ILX::call $ilx_handle "my_method" $user $passwd} result]} { log local0.error "Client - [IP::client_addr], ILX failure: $result" # Send user graceful error message with whatever code, then exit event return }
You can see on line 3, we make the
to the extension by using our RPC handle, specifying the method ILX::call
(our "remote procedure") and sending it one or more args (in this case my_method
and user
which we got somewhere else). In this example we have wrapped passwd
in a ILX::call
command because it can throw an exception if there is something wrong with the RPC (Note: catch should actually be used on any command that could throw an exception, b64decode for example). This allows us to gracefully handle errors and respond back to the client in a more controlled manner. If catch
is successful, then the return of ILX::call
will be a 0 and the result of the command will be stored in the variable catch
. If ILX::call fails, then the return of result
will be a 1 and the variable catch
will contain the error message. This will cause the code in the result
block to execute which would be our error handling.if
Assuming everything went well, we could now start working with data in the variable
. If we returned a single value from our RPC, then we could process this data as a string like so -result
# If one value is returned, it becomes TCL string if {$result eq 'yes'} { # Some action }
But if we return multiple values from our RPC, these would be in TCL list format (we will talk more about how to return multiple values in the Node.js coding section). You could use
or any suitable TCL list command on the variable lindex
-result
# If multiple values are returned, it becomes TCL list set x [ lindex $result 0] set y [ lindex $result 1]
Node.js Coding
On the Node.js side, we would write code in our index.js file of the extension in the workspace. A code template will load when the extension is created to give you a starting point, so you dont have to write it from scratch.
To use Node.js in iRules LX, we provide an API for receiveing and sending data from the TCL RPC. Here is an example -
var f5 = require('f5-nodejs'); var ilx = new f5.ILXServer(); ilx.addMethod('my_method', function (req, res) { // req.params() function is used to get values from TCL. Returns JS Array var user = req.params()[0]; var passwd = req.params()[1]; <DO SOMETHING HERE> res.reply('<value>'); // Return one value as string or number res.reply(['value1','value2','value3']); // Return multiple values with an Array }); ilx.listen();
Now we will run through this step-by-step.
On line 1, you see we import the
module which provides our API to the ILX server.f5-nodejs
var f5 = require('f5-nodejs');
On line 3 we instantiate a new instance of the
class and store the object in the variable ILXServer
.ilx
var ilx = new f5.ILXServer();
On line 5, we have our
method which stores methods (our remote procedures) in our ILX server. addMethod
ilx.addMethod('my_method', function (req, res) { // req.params() function is used to get values from TCL. Returns JS Array var user = req.params()[0]; var passwd = req.params()[1]; <DO SOMETHING HERE> res.reply('<value>'); // Return one value as string or number res.reply(['value1','value2','value3']); // Return multiple values with an Array });
This is where we would write our custom Node.js code for our use case. This method takes 2 arguments -
- Method name - This would be the name that we call from TCL. If you remember from our TCL code above in line 3 we call the method
. This matches the name we put here.my_method
- Callback function - This is the function that will get executed when we make the RPC to this method. This function gets 2 arguments, the
andreq
objects which follow standard Node.js conventions for the request and response objects.res
In order to get our data that we sent from TCL, we must call the
method. This will return an array and the number of elements in that array will match the number of arguments we sent in req.param
. In our TCL example on line 3, we sent the variables ILX::call
and user
which we got somewhere else. That means that the array from passwd
will have 2 elements, which we assign to variables in lines 7-8. Now that we have our data, we can use any valid JavaScript to process it.req.params
Once we have a result and want to return something to TCL, we would use the
method. We have both ways of using res.reply
shown on lines 12-13, but you would only use one of these depending on how many values you wish to return. On line 12, you would put a string or number as the argument for res.reply
if you wanted to return a single value. If we wished to return multiple values, then we would use an array with strings or numbers. These are the only valid data types for res.reply
.res.reply
We mentioned in the TCL result that we could get one value that would be a string or multiple values that would be a TCL list. The argument type you use in res.reply is how you would determine that.
Then on line 16 we start our ILX server so that it will be ready to listen to RPC.
ilx.listen();
That was quick a overview of the F5 API for Node.js in iRules LX. It is important to note that F5 will only support using Node.js in iRules LX within the provided API.
A Real Use Case
Now we can take what we just learned and actually do something useful. In our example, we will take POST data from a standard HTML form and convert it to JSON. In TCL we would intercept the data, send it to Node.js to transform it to JSON, then return it to TCL to replace the POST data with the JSON and change the Content-Type header -
when HTTP_REQUEST { # Collect POST data if { [HTTP::method] eq "POST" }{ set cl [HTTP::header "Content-Length"] HTTP::collect $cl } } when HTTP_REQUEST_DATA { # Send data to Node.js set handle [ILX::init "json_plugin" "json_ext"] if {[catch {ILX::call $handle "post_transform" [HTTP::payload]} json]} { log local0.error "Client - [IP::client_addr], ILX failure: $json" HTTP::respond 400 content "<html>Some error page to client</html>" return } # Replace Content-Type header and POST payload HTTP::header replace "Content-Type" "application/json" HTTP::payload replace 0 $cl $json }
In Node.js, would only need to load the built in module
to parse the post data and then querystring
to turn it into JSON.JSON.stringify
'use strict' var f5 = require('f5-nodejs'); var qs = require('querystring'); var ilx = new f5.ILXServer(); ilx.addMethod('post_transform', function (req, res) { // Get POST data from TCL and parse query into a JS object var postData = qs.parse(req.params()[0]); // Turn postData into JSON and return to TCL res.reply(JSON.stringify(postData)); }); ilx.listen();
Note: Keep in mind that this is only a basic example. This would not handle a POST that used 100 continue or mutlipart POSTs.
Exception Handling
iRules TCL is very forgiving when there is an unhanded exception. When you run into an unhandled runtime exception (such as an invalid base64 string you tried to decode), you only reset that connection. However, Node.js (like most other programming languages) will crash if you have an unhandled runtime exception, so you will need to put some guard rails in your code to avoid this. Lets say for example you are doing
JSON.parse
of some JSON you get from the client. Without proper exception handling any client could crash your Node.js process by sending invalid JSON. In iRules LX if a Node.js process crashes 5 times in 60 seconds, BIG-IP will not attempt to restart it which opens up a DoS attack vector on your application (max restarts is user configurable, but good code eliminates the need to change it). You would have to manually restart it via the Web UI or TMSH.
In order to catch errors in JavaScript, you would use the try/catch statement. There is one caveat to this: code inside a try/catch statement is not optimized by the v8 complier and will cause a significant decrease in performance. Therefore, we should keep our code in try/catch to a minimum by putting only the functions that throw exceptions in the statement. Usually, any function that will take user provided input can throw.
Note: The subject of code optimization with v8 is quite extensive so we will only talk about this one point. There are many blog articles about v8 optimization written by people much smarter than me. Use your favorite search engine with the keywords v8 optimization to find them.
Here is an example of try/catch with
JSON.parse
-
ilx.addMethod('my_function', function (req, res) { try { var myJson = JSON.parse(req.params()[0]) // This function can throw } catch (e) { // Log message and stop processing function console.error('Error with JSON parse:', e.message); console.error('Stack trace:', e.stack); return; } // All my other code is outside try/catch var result = ('myProperty' in myJson) ? true : false; res.reply(result); });
We can also work around the optimization caveat by hoisting a custom function outside try/catch and calling it inside the statement -
ilx.addMethod('my_function', function (req, res) { try { var answer = someFunction(req.params()[0]) // Call function from here that is defined on line 16 } catch (e) { // Log message an stop processing function console.error('Error with someFunction:', e.message); console.error('Stack trace:', e.stack); return; } // All my other code is outside try/catch var result = (answer === 'hello') ? true : false; res.reply(result); }); function someFuntion (arg) { // Some code in here that can throw return result }
RPC Status Return Value
In our examples above, we simply stopped the function call if we had an error but never let TCL know that we encountered a problem. TCL would not know there was a problem until the
ILX::call
command reached it's timeout value (3 seconds by default). The client connection would be held open until it reached the timeout and then reset. While it is not required, it would be a good idea for TCL to get a return value on the status of the RPC immediately. The specifics of this is pretty open to any method you can think of but we will give an example here.
One way we can accomplish this is by the return of multiple values from Node.js. Our first value could be some type of RPC status value (say an RPC error value) and the rest of the value(s) could be our result from the RPC. It is quite common in programming that make an error value would be 0 if everything was okay but would be a positive integer to indicate a specific error code.
Here in this example, we will demonstate that concept. The code will verify that the property
myProperty
is present in JSON data and put it's value into a header or send a 400 response back to the client if not.
THe Node.js code -
ilx.addMethod('check_value', function (req, res) { try { var myJson = JSON.parse(req.params()[0]) // This function can throw } catch (e) { res.reply(1); //<---------------------------------- The RPC error value is 1 indicating invalid JSON return; } if ('myProperty' in myJson){ // The myProperty property was present in the JSON data, evaluate its value var result = (myJson.myProperty === 'hello') ? true : false; res.reply([0, result]); //<-------------------------- The RPC error value is 0 indicating success } else { res.reply(2); //<-------------- The RPC error value is 2 indicating myProperty was not present } });
In the code above the first value we return to TCL is our RPC error code. We have defined 3 possible values for this -
- 0 - RPC success
- 1 - Invalid JSON
- 2 - Property "myProperty" not present in JSON
One our TCL side we would need to add logic to handle this value -
when HTTP_REQUEST { # Collect POST data if { [HTTP::method] eq "POST" }{ set cl [HTTP::header "Content-Length"] HTTP::collect $cl } } when HTTP_REQUEST_DATA { # Send data to Node.js set handle [ILX::init "json_plugin" "json_checker"] if {[catch [ILX::call $handle "check_value" [HTTP::payload]] json]} { log local0.error "Client - [IP::client_addr], ILX failure: $result" HTTP::respond 400 content "<html>Some error page to client</html>" return } # Check the RPC error value if {[lindex $json 0] > 0} { # RPC error value was not 0, there is a problem switch [lindex $json 0] { 1 { set error_msg "Invalid JSON"} 2 { set error_msg "myProperty property not present"} } HTTP::respond 400 content "<html>The following error occured: $error_msg</html>" } else { # If JSON was okay, insert header with myProperty value HTTP::header insert "X-myproperty" [lindex $json 1] } }
As you can see on line 19, we check the value of the first element in our TCL list. If it is greater than 0 then we know we have a problem. We move on down further to line 20 to determine the problem and set a variable that will become part of our error message back to the client.
Note: Keep in mind that this is only a basic example. This would not handle a POST that used 100 continue or mutlipart POSTs.
In the next article in this series, we will cover how to install a module with NPM and some best practices.
- WIPAWAT_UPPATU1Nimbostratus
Thanks
- linjingEmployee
Finish all series, and waiting next :)