Skip to content

Instantly share code, notes, and snippets.

@nginx-gists
Last active February 16, 2023 03:01
Show Gist options
  • Save nginx-gists/ffad47e81322d6b948ee187e56a39600 to your computer and use it in GitHub Desktop.
Save nginx-gists/ffad47e81322d6b948ee187e56a39600 to your computer and use it in GitHub Desktop.
Batching API Requests with NGINX Plus and the NGINX JavaScript Module
js_import batch-api-min.js;
# keyval_zone for APIs where the last portion of the URI is an argument
# The key is the portion of the URL before the last part
keyval_zone zone=batch_api:64k state=/etc/nginx/state-files/batch-api.json;
keyval $uri_prefix $batch_api zone=batch_api;
# keyval_zone for APIs where the last portion of the URI is an argument
# The key is the URI
keyval_zone zone=batch_api2:64k state=/etc/nginx/state-files/batch-api2.json;
keyval $uri $batch_api2 zone=batch_api2;
map $uri $uri_prefix {
~^(?<p>.+)\/.+$ $p;
}
map $uri $uri_suffix {
~^.+\/(?<s>.+)$ $s;
}
upstream api_servers {
zone api_servers 64k;
server 127.0.0.1:9080;
server 127.0.0.1:9081;
}
server {
listen 80;
location /batch-api {
set $batch_api_arg_in_uri on;
js_content batch-api-min.batchAPI;
}
location /batch-api2 {
set $batch_api_arg_in_uri off;
js_content batch-api-min.batchAPI;
}
location /myapi {
proxy_pass http://api_servers;
}
location /api {
api write=on;
# directives to restrict access to the API
}
}
# vim: syntax=nginx
/*******************************************************************************
* Copyright (C) 2019 NGINX, Inc., 2022 F5, Inc.
* This program is provided for demonstration purposes only.
*******************************************************************************/
function batchAPI(r) {
var n = 0, requestCount = 0;
var resp = "[";
var errorOccured = false;
var keyval = "batch_api";
function done(reply) { // Callback for completed subrequests
n++;
if (errorOccured) { /* Once one response has an error stop processing
any more responses */
return;
}
if (n < requestCount) {
if (reply.status != 200) {
errorOccured = true;
r.log("Error in response " + n.toString() + " " + reply.uri +
" " + reply.status.toString());
r.return(reply.status, "Error in response " + n.toString() +
" " + reply.uri + "\n");
} else {
resp += '["' + reply.uri + '",' + reply.body + '],';
}
} else { // Last response
if (reply.status != 200) {
errorOccured = true;
r.log("Error in response " + n.toString() + " " + reply.uri +
" " + reply.status.toString());
r.return(reply.status, "Error in response " + n.toString() +
" " + reply.uri + "\n");
} else {
resp += '["' + reply.uri + '",' + reply.body + ']';
r.return(200, resp);
}
}
}
var argInURI = r.variables.batch_api_arg_in_uri.toLowerCase();
if (argInURI != "on") {
keyval = "batch_api2";
}
var apiURIs = r.variables[keyval].split(",");
requestCount = apiURIs.length;
for (var i = 0; i < requestCount; i++) {
if (argInURI == "on") {
r.subrequest(apiURIs[i] + "/" + r.variables.uri_suffix,
r.variables.args, done);
} else {
r.subrequest(apiURIs[i], r.variables.args, done);
}
}
}
export default { batchAPI }
js_import batch-api.js;
# keyval_zone for APIs where the last portion of the URI is an argument
# The key is the portion of the URL before the last part set in the map
keyval_zone zone=batch_api:64k state=/etc/nginx/state-files/batch-api.json;
keyval $uri_prefix $batch_api zone=batch_api;
# keyval_zone for APIs where the last portion of the URI is an argument
# The key is the URI
keyval_zone zone=batch_api2:64k state=/etc/nginx/state-files/batch-api2.json;
keyval $uri $batch_api2 zone=batch_api2;
# These maps are for breaking the URI into two parts for APIs where the
# last part of the URI is an argument. For URIs of the form:
# /<part 1>/<part 2>/<part n>
# $uri_prefix = /<part 1>/<part 2>
# $uri_suffix = <part n>
map $uri $uri_prefix {
~^(?<p>.+)\/.+$ $p;
}
map $uri $uri_suffix {
~^.+\/(?<s>.+)$ $s;
}
upstream api_servers {
zone api_servers 64k;
server 127.0.0.1:9080;
server 127.0.0.1:9081;
}
upstream services {
zone services 64k;
server 127.0.0.1:9000;
}
server {
listen 9000;
location / {
rewrite ^(.+)\/(.+)$ $1.php?item=$2 last;
}
location ~ \.php$ {
proxy_pass http://api_servers;
}
}
server {
listen 80;
set $batch_api_verbose on;
location /batch-api {
set $batch_api_arg_in_uri on;
js_content batch-api.batchAPI;
}
location /batch-api2 {
set $batch_api_arg_in_uri off;
js_content batch-api.batchAPI;
}
location /myapi {
proxy_pass http://services;
}
location /api {
api write=on;
# directives to restrict access to the API
}
}
# vim: syntax=nginx
/*******************************************************************************
* Copyright (C) 2019 NGINX, Inc., 2022 F5, Inc.
*
* This program is provided for demonstration purposes only.
*
* Make a series of API subrequests based on one input request and return an
* aggregated response in JSON format as list.
*
* Two styles of API requests are supported:
* 2. The last portion of the URI is a value to be included in all
* requests. For example, a request to /batch-api/product/123 will result
* in requests to /myapi/catalog/123, /myapi/inventory/123 and
* /myapi/review/123.
* 1. The URI specifies a endpoint and arguments required are passed in the
* query string. For example, a request to /batch-api2/product?item=123
* will result in requests for /myapi/catalog.php?item=123,
* /myapi/inventory.php?item=123 and /myapi/review.php?item=123. Some
* APIs may pass required arguments in a prior request so this code does
* not require any arguments.
*
* All request arguments included in the intiial request are passed to all the
* batched requests.
*
* The NGINX variable batch_api_arg_in_uri controls which of the above two styles
* to use. The first style is specified with a value of "o n" and the second
* with a value of "off". The default is "on".
*
* The NGINX Plus key-value store is used to keep mappings of input URIs and
* a comma seperated list of the API URIs to call.
*
* For the first style, the key
* is the value of the URI before the last slash. For example, a request for
* /batch-api/product/123 would have a key of /batch-api/product. An NGINX
* map is used to get the value of the key as the variable $uri_prefix and
* the value of the argument as $uri_suffix.
*
* For the second style, the key is the URI. For example, a request to
* /batch-api2/product?item=123 would have a key of /batch-api2/product.
*
* The name of the key-value store for the first style is "batch_api" and for
* the second it is "batch_api2". These names could be set using NGINX
* variables, but for simplicity they have been hardcoded.
*
* The subrequests are made in order, but they are asynchronous so the requests
* are executed in parallel so the responses may come out of order.
*
* If there is an error for any requests, an error is returned to the client
* and the none of the data from any of the responses is returned.
*
* A message to the NGINX error log is written for any errors. Additional
* informationa messages will be written to the error log if the NGINX variable
* batchapi_verbose is set to "on".
*
* NGINX Plus Key-Value Store
*
* Example NGINX Plus configuration - style 1:
* keyval_zone zone=batch_api:64k;
* keyval $uri_prefix $batch_api zone=batch_api;
*
* Data:
* Key: "/batch-api/product"
* Value: "/myapi/catalog,/myapi/inventory,/myapi/review"
*
* Example NGINX Plus configuration - style 2:
* keyval_zone zone=batch_api2:64k;
* keyval $uri $batch_api2 zone=batch_api2;
*
* Data:
* Key: "/batch-api2/product"
* Value: "/myapi/catalog.php,/myapi/inventory.php,/myapi/review.php"
*******************************************************************************/
/*******************************************************************************
* Make a series of API requests as defined in the NGINX Plus key value store
*******************************************************************************/
function batchAPI(r) {
var n = 0, requestCount = 0;
var resp = "[";
var errorOccured = false;
var keyval = "batch_api";
function done(reply) { // Callback for completed subrequests
n++;
if (errorOccured) { /* Once one response has an error stop processing
any more responses */
logVerbose(verbose, r, "Response " + n.toString() + " " +
reply.uri + " " + reply.status.toString() +
" Error occured in previous request");
return;
}
if (n < requestCount) {
if (reply.status != 200) {
errorOccured = true;
r.log("Error in response " + n.toString() + " " + reply.uri +
" " + reply.status.toString());
r.return(reply.status, "Error in response " + n.toString() +
" " + reply.uri + "\n");
} else {
logVerbose(verbose, r, "Response " + n.toString() + " " +
reply.uri + " " + reply.status.toString());
resp += '["' + reply.uri + '",' + reply.body + '],';
}
} else { // Last response
if (reply.status != 200) {
errorOccured = true;
r.log("Error in response " + n.toString() + " " + reply.uri +
" " + reply.status.toString());
r.return(reply.status, "Error in response " + n.toString() +
" " + reply.uri + "\n");
} else {
logVerbose(verbose, r, "Response " + n.toString() + " " +
reply.uri + " " + reply.status.toString());
resp += '["' + reply.uri + '",' + reply.body + ']';
logVerbose(verbose, r, "Aggregated Response: " + resp);
r.return(200, resp);
}
}
}
var argInURI = r.variables.batch_api_arg_in_uri.toLowerCase();
if (argInURI != "on") {
keyval = "batch_api2";
}
var verbose = r.variables.batch_api_verbose.toLowerCase();
logVerbose(verbose, r, "argInURI: " + argInURI + " keyval: " + keyval);
logVerbose(verbose, r, "prefix: " + r.variables.uri_prefix +
" suffix: " + r.variables.uri_suffix);
// There must be a key value for the request uri
if (r.variables[keyval] == '') {
r.log("Error: No value for " + r.uri + " in key-value store " +
keyval);
r.return(400, "Invalid request: No API requests specified for " +
r.variables.uri + "\n");
} else {
logVerbose(verbose, r, "API URIs: " + r.variables[keyval]);
logVerbose(verbose, r, "Args: " + r.variables.args);
var apiURIs = r.variables[keyval].split(",");
requestCount = apiURIs.length;
for (var i = 0; i < requestCount; i++) {
if (argInURI == "on") {
logVerbose(verbose, r, "Subrequest: " + apiURIs[i] + "/" +
r.variables.uri_suffix + "?" + r.variables.args);
r.subrequest(apiURIs[i] + "/" + r.variables.uri_suffix,
r.variables.args, done);
} else {
logVerbose(verbose, r, "Subrequest: " + apiURIs[i] + "?" +
r.variables.args);
r.subrequest(apiURIs[i], r.variables.args, done);
}
}
}
}
/*******************************************************************************
* Log Verbose Message
*******************************************************************************/
function logVerbose(verbose, r, message) {
if (verbose == "on") {
r.log(message);
}
}
export default { batchAPI }
<?php
if (isset($_GET['item'])) {
$item = $_GET['item'];
} else {
$item = 'unset';
}
if (isset($_GET['x'])) {
$x = $_GET['x'];
} else {
$x = 'unset';
}
print('{"service":"Catalog","item":"' . $item . '"}' . "\n");
?>
<?php
if (isset($_GET['item'])) {
$item = $_GET['item'];
} else {
$item = 'unset';
}
print('{"service":"Inventory","item":"' . $item . '"}' . "\n");
?>
<?php
if (isset($_GET['item'])) {
$item = $_GET['item'];
} else {
$item = 'unset';
}
print('{"service":"Review","item":"' . $item . '"}' . "\n");
?>
{
"applications": {
"batch-api": {
"type": "php",
"workers": 20,
"root": "/srv/app/content",
"limits": {
"timeout": 65
}
}
},
"listeners": {
"*:9080": {
"application": "batch-api"
}
}
}
@nginx-gists
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment