Modifying Request and Response
As we’ve seen in the previous examples, actions get called with the request andresponse objects (named req and res in the examples) passed as parameters totheir handler functions.
The req object contains the incoming HTTP request, which might or might nothave been modified by a previous action (if actions were chained).
A handler can modify the request object in place if desired. This might beuseful when writing middleware (see below) that is used to intercept incomingrequests, modify them and pass them to the actual handlers.
While modifying the request object might not be that relevant for non-middlewareactions, modifying the response object definitely is. Modifying the responseobject is an action’s only way to return data to the caller of the action.
We’ve already seen how to set the HTTP status code, the content type, and theresult body. The res object has the following properties for these:
- contentType: MIME type of the body as defined in the HTTP standard (e.g.text/html, text/plain, application/json, …)
- responsecode: the HTTP status code of the response as defined in the HTTPstandard. Common values for actions that succeed are 200 or 201.Please refer to the HTTP standard for more information.
- body: the actual response dataTo set or modify arbitrary headers of the response object, the _headers_property can be used. For example, to add a user-defined header to the response,the following code will do:
res.headers = res.headers || { }; // headers might or might not be present
res.headers['X-Test'] = 'someValue'; // set header X-Test to "someValue"
This will set the additional HTTP header X-Test to value someValue. Otherheaders can be set as well. Note that ArangoDB might change the case of theheader names to lower case when assembling the overall response that is sent tothe caller.
It is not necessary to explicitly set a Content-Length header for the responseas ArangoDB will calculate the content length automatically and add this headeritself. ArangoDB might also add a Connection header itself to handle HTTPkeep-alive.
ArangoDB also supports automatic transformation of the body data to anotherformat. Currently, the only supported transformations are base64-encoding andbase64-decoding. Using the transformations, an action can create a base64encoded body and still let ArangoDB send the non-encoded version, for example:
res.body = 'VGhpcyBpcyBhIHRlc3Q=';
res.transformations = res.transformations || [ ]; // initialize
res.transformations.push('base64decode'); // will base64 decode the response body
When ArangoDB processes the response, it will base64-decode what’s in res.body_and set the HTTP header _Content-Encoding: binary. The opposite can be achievedwith the base64encode transformation: ArangoDB will then automaticallybase64-encode the body and set a Content-Encoding: base64 HTTP header.
Writing dynamic action handlers
To write your own dynamic action handlers, you must put them into modules.
Modules are a means of organizing action handlers and making them loadable underspecific names.
To start, we’ll define a simple action handler in a module /ownTest:
- arangosh> db._modules.save({
- ........> path: "/db:/ownTest",
- ........> content:
- ........> "exports.do = function(req, res, options, next) {"+
- ........> " res.body = 'test';" +
- ........> " res.responseCode = 200;" +
- ........> " res.contentType = 'text/plain';" +
- ........> "};"
- ........> });
Show execution results
- {
- "_id" : "_modules/68346",
- "_key" : "68346",
- "_rev" : "_ZP4PGxq---"
- }
Hide execution results
This does nothing but register a do action handler in a module /ownTest. Theaction handler is not yet callable, but must be mapped to a route first. To mapthe action to the route /ourtest, execute the following command:
- arangosh> db._routing.save({
- ........> url: "/ourtest",
- ........> action: {
- ........> controller: "db://ownTest"
- ........> }
- ........> });
- arangosh> require("internal").reloadRouting()
Show execution results
- {
- "_id" : "_routing/68349",
- "_key" : "68349",
- "_rev" : "_ZP4PGx2---"
- }
Hide execution results
Now use the browser or cURL and access http://localhost:8529/ourtest :
Show execution results
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/ourtest
- HTTP/1.1 OK
- content-type: text/plain
- x-content-type-options: nosniff
- "test"
Hide execution results
You will see that the module’s do function has been executed.
A Word about Caching
Sometimes it might seem that your change do not take effect. In this case theculprit could be the routing caches:
The routing cache stores the routing information computed from the __routing_collection. Whenever you change this collection manually, you need to call
- arangosh> require("internal").reloadRouting()
Show execution results
Hide execution results
in order to rebuild the cache.
Advanced Usages
For detailed information see the reference manual.
Redirects
Use the following for a permanent redirect:
- arangosh> db._routing.save({
- ........> url: "/redirectMe",
- ........> action: {
- ........> do: "@arangodb/actions/redirectRequest",
- ........> options: {
- ........> permanently: true,
- ........> destination: "/somewhere.else/"
- ........> }
- ........> }
- ........> });
- arangosh> require("internal").reloadRouting()
Show execution results
- {
- "_id" : "_routing/68362",
- "_key" : "68362",
- "_rev" : "_ZP4PG0C---"
- }
Hide execution results
Show execution results
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/redirectMe
- HTTP/1.1 Moved Permanently
- content-type: text/html
- location: /somewhere.else/
- x-content-type-options: nosniff
- "<html><head><title>Moved</title></head><body><h1>Moved</h1><p>This page has moved to <a href=\"/somewhere.else/\">/somewhere.else/</a>.</p></body></html>"
Hide execution results
Routing Bundles
Instead of adding all routes for package separately, you canspecify a bundle:
- arangosh> db._routing.save({
- ........> routes: [
- ........> {
- ........> url: "/url1",
- ........> content: "route 1"
- ........> },
- ........> {
- ........> url: "/url2",
- ........> content: "route 2"
- ........> },
- ........> {
- ........> url: "/url3",
- ........> content: "route 3"
- ........> }
- ........> ]
- ........> });
- arangosh> require("internal").reloadRouting()
Show execution results
- {
- "_id" : "_routing/68371",
- "_key" : "68371",
- "_rev" : "_ZP4PG2G---"
- }
Hide execution results
Show execution results
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/url2
- HTTP/1.1 OK
- content-type: text/plain
- x-content-type-options: nosniff
- "route 2"
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/url3
- HTTP/1.1 OK
- content-type: text/plain
- x-content-type-options: nosniff
- "route 3"
Hide execution results
The advantage is, that you can put all your routes into one documentand use a common prefix.
- arangosh> db._routing.save({
- ........> urlPrefix: "/test",
- ........> routes: [
- ........> {
- ........> url: "/url1",
- ........> content: "route 1"
- ........> },
- ........> {
- ........> url: "/url2",
- ........> content: "route 2"
- ........> },
- ........> {
- ........> url: "/url3",
- ........> content: "route 3"
- ........> }
- ........> ]
- ........> });
- arangosh> require("internal").reloadRouting()
Show execution results
- {
- "_id" : "_routing/68380",
- "_key" : "68380",
- "_rev" : "_ZP4PG4K---"
- }
Hide execution results
will define the URL /test/url1, /test/url2, and /test/url3:
Show execution results
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/test/url1
- HTTP/1.1 OK
- content-type: text/plain
- x-content-type-options: nosniff
- "route 1"
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/test/url2
- HTTP/1.1 OK
- content-type: text/plain
- x-content-type-options: nosniff
- "route 2"
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/test/url3
- HTTP/1.1 OK
- content-type: text/plain
- x-content-type-options: nosniff
- "route 3"
Hide execution results
Writing Middleware
Assume, you want to log every request in your namespace to the console. (if ArangoDB is runningas a daemon, this will end up in the logfile). In this case you can easily define anaction for the URL /subdirectory. This action simply logsthe requests, calls the next in line, and logs the response:
- arangosh> db._modules.save({
- ........> path: "/db:/OwnMiddlewareTest",
- ........> content:
- ........> "exports.logRequest = function (req, res, options, next) {" +
- ........> " console = require('console'); " +
- ........> " console.log('received request: %s', JSON.stringify(req));" +
- ........> " next();" +
- ........> " console.log('produced response: %s', JSON.stringify(res));" +
- ........> "};"
- ........> });
Show execution results
- {
- "_id" : "_modules/68389",
- "_key" : "68389",
- "_rev" : "_ZP4PG6e---"
- }
Hide execution results
This function will now be available as db://OwnMiddlewareTest/logRequest. You need totell ArangoDB that it is should use a prefix match and that the shortest matchshould win in this case:
- arangosh> db._routing.save({
- ........> middleware: [
- ........> {
- ........> url: {
- ........> match: "/subdirectory/*"
- ........> },
- ........> action: {
- ........> do: "db://OwnMiddlewareTest/logRequest"
- ........> }
- ........> }
- ........> ]
- ........> });
Show execution results
- {
- "_id" : "_routing/68392",
- "_key" : "68392",
- "_rev" : "_ZP4PG6q---"
- }
Hide execution results
When you call next() in that action, the next specific routing willbe used for the original URL. Even if you modify the URL in the requestobject req, this will not cause the next() to jump to the routingdefined for this next URL. If proceeds occurring the origin URL. However,if you use next(true), the routing will stop and request handling isstarted with the new URL. You must ensure that next(true) is nevercalled without modifying the URL in the request objectreq. Otherwise an endless loop will occur.
Now we add some other simple routings to test all this:
- arangosh> db._routing.save({
- ........> url: "/subdirectory/ourtest/1",
- ........> action: {
- ........> do: "@arangodb/actions/echoRequest"
- ........> }
- ........> });
- arangosh> db._routing.save({
- ........> url: "/subdirectory/ourtest/2",
- ........> action: {
- ........> do: "@arangodb/actions/echoRequest"
- ........> }
- ........> });
- arangosh> db._routing.save({
- ........> url: "/subdirectory/ourtest/3",
- ........> action: {
- ........> do: "@arangodb/actions/echoRequest"
- ........> }
- ........> });
- arangosh> require("internal").reloadRouting()
Show execution results
- {
- "_id" : "_routing/68395",
- "_key" : "68395",
- "_rev" : "_ZP4PG62---"
- }
- {
- "_id" : "_routing/68397",
- "_key" : "68397",
- "_rev" : "_ZP4PG66---"
- }
- {
- "_id" : "_routing/68399",
- "_key" : "68399",
- "_rev" : "_ZP4PG66--A"
- }
Hide execution results
Then we send some curl requests to these sample routes:
Show execution results
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/subdirectory/ourtest/1
- HTTP/1.1 OK
- content-type: application/json; charset=utf-8
- x-content-type-options: nosniff
- {
- "request" : {
- "authorized" : true,
- "user" : "root",
- "database" : "_system",
- "url" : "/subdirectory/ourtest/1",
- "protocol" : "http",
- "server" : {
- "address" : "127.0.0.1",
- "port" : 37891
- },
- "client" : {
- "address" : "127.0.0.1",
- "port" : 44042,
- "id" : "156817587559043"
- },
- "internals" : {
- },
- "headers" : {
- "host" : "127.0.0.1",
- "connection" : "Keep-Alive",
- "accept" : "application/json",
- "authorization" : "Basic cm9vdDo=",
- "content-type" : "application/json"
- },
- "requestType" : "GET",
- "parameters" : {
- },
- "cookies" : {
- },
- "urlParameters" : {
- }
- },
- "options" : {
- }
- }
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/subdirectory/ourtest/2
- HTTP/1.1 OK
- content-type: application/json; charset=utf-8
- x-content-type-options: nosniff
- {
- "request" : {
- "authorized" : true,
- "user" : "root",
- "database" : "_system",
- "url" : "/subdirectory/ourtest/2",
- "protocol" : "http",
- "server" : {
- "address" : "127.0.0.1",
- "port" : 37891
- },
- "client" : {
- "address" : "127.0.0.1",
- "port" : 44042,
- "id" : "156817587559043"
- },
- "internals" : {
- },
- "headers" : {
- "host" : "127.0.0.1",
- "connection" : "Keep-Alive",
- "accept" : "application/json",
- "authorization" : "Basic cm9vdDo=",
- "content-type" : "application/json"
- },
- "requestType" : "GET",
- "parameters" : {
- },
- "cookies" : {
- },
- "urlParameters" : {
- }
- },
- "options" : {
- }
- }
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/subdirectory/ourtest/3
- HTTP/1.1 OK
- content-type: application/json; charset=utf-8
- x-content-type-options: nosniff
- {
- "request" : {
- "authorized" : true,
- "user" : "root",
- "database" : "_system",
- "url" : "/subdirectory/ourtest/3",
- "protocol" : "http",
- "server" : {
- "address" : "127.0.0.1",
- "port" : 37891
- },
- "client" : {
- "address" : "127.0.0.1",
- "port" : 44042,
- "id" : "156817587559043"
- },
- "internals" : {
- },
- "headers" : {
- "host" : "127.0.0.1",
- "connection" : "Keep-Alive",
- "accept" : "application/json",
- "authorization" : "Basic cm9vdDo=",
- "content-type" : "application/json"
- },
- "requestType" : "GET",
- "parameters" : {
- },
- "cookies" : {
- },
- "urlParameters" : {
- }
- },
- "options" : {
- }
- }
Hide execution results
and the console (and / or the logfile) will show requests and replies.Note that logging doesn’t warrant the sequence in which these lineswill appear.
Application Deployment
Using single routes or bundles can bebecome a bit messy in large applications. Kaerus has written a deployment tool in node.js.
Note that there is also Foxx for building applicationswith ArangoDB.
Common Pitfalls when using Actions
Caching
If you made any changes to the routing but the changes does not have any effectwhen calling the modified actions URL, you might have been hit by somecaching issues.
After any modification to the routing or actions, it is thus recommended tomake the changes “live” by calling the following functions from within arangosh:
Show execution results
Hide execution results
You might also be affected by client-side caching.Browsers tend to cache content and also redirection URLs. You might need toclear or disable the browser cache in some cases to see your changes in effect.
Data types
When processing the request data in an action, please be aware that the datatype of all query parameters is string. This is because the whole URL is astring and when the individual parts are extracted, they will also be strings.
For example, when calling the URL http:// localhost:8529/hello/world?value=5
the parameter value will have a value of (string) 5, not (number) 5.This might be troublesome if you use JavaScript’s === operator when checkingrequest parameter values.
The same problem occurs with incoming HTTP headers. When sending the followingheader from a client to ArangoDB
X-My-Value: 5
then the header X-My-Value will have a value of (string) 5 and not (number) 5.
404 Not Found
If you defined a URL in the routing and the URL is accessible fine viaHTTP GET but returns an HTTP 501 (not implemented) for other HTTP methodssuch as POST, PUT or DELETE, then you might have been hit by somedefaults.
By default, URLs defined like this (simple string url attribute) areaccessible via HTTP GET and HEAD only. To make such URLs accessible viaother HTTP methods, extend the URL definition with the methods attribute.
For example, this definition only allows access via GET and HEAD:
{
url: "/hello/world"
}
whereas this definition allows HTTP GET, POST, and PUT:
- arangosh> db._routing.save({
- ........> url: {
- ........> match: "/hello/world",
- ........> methods: [ "get", "post", "put" ]
- ........> },
- ........> action: {
- ........> do: "@arangodb/actions/echoRequest"
- ........> }
- ........> });
- arangosh> require("internal").reloadRouting()
Show execution results
- {
- "_id" : "_routing/68415",
- "_key" : "68415",
- "_rev" : "_ZP4PG96---"
- }
Hide execution results
Show execution results
- shell> curl --header 'accept: application/json' --dump - http://localhost:8529/hello/world
- HTTP/1.1 OK
- content-type: application/json; charset=utf-8
- x-content-type-options: nosniff
- {
- "request" : {
- "authorized" : true,
- "user" : "root",
- "database" : "_system",
- "url" : "/hello/world",
- "protocol" : "http",
- "server" : {
- "address" : "127.0.0.1",
- "port" : 37891
- },
- "client" : {
- "address" : "127.0.0.1",
- "port" : 44042,
- "id" : "156817587559043"
- },
- "internals" : {
- },
- "headers" : {
- "host" : "127.0.0.1",
- "connection" : "Keep-Alive",
- "accept" : "application/json",
- "authorization" : "Basic cm9vdDo=",
- "content-type" : "application/json"
- },
- "requestType" : "GET",
- "parameters" : {
- },
- "cookies" : {
- },
- "urlParameters" : {
- }
- },
- "options" : {
- }
- }
- shell> curl -X POST --header 'accept: application/json' --data-binary @- --dump - http://localhost:8529/hello/world <<EOF
- {hello: 'world'}
- EOF
- HTTP/1.1 OK
- content-type: application/json; charset=utf-8
- x-content-type-options: nosniff
- {
- "request" : {
- "authorized" : true,
- "user" : "root",
- "database" : "_system",
- "url" : "/hello/world",
- "protocol" : "http",
- "server" : {
- "address" : "127.0.0.1",
- "port" : 37891
- },
- "client" : {
- "address" : "127.0.0.1",
- "port" : 44042,
- "id" : "156817587559043"
- },
- "internals" : {
- },
- "headers" : {
- "host" : "127.0.0.1",
- "content-length" : "16",
- "connection" : "Keep-Alive",
- "accept" : "application/json",
- "authorization" : "Basic cm9vdDo=",
- "content-type" : "application/json"
- },
- "requestType" : "POST",
- "requestBody" : "{hello: 'world'}",
- "parameters" : {
- },
- "cookies" : {
- },
- "urlParameters" : {
- }
- },
- "options" : {
- }
- }
- shell> curl -X PUT --header 'accept: application/json' --data-binary @- --dump - http://localhost:8529/hello/world <<EOF
- {hello: 'world'}
- EOF
- HTTP/1.1 OK
- content-type: application/json; charset=utf-8
- x-content-type-options: nosniff
- {
- "request" : {
- "authorized" : true,
- "user" : "root",
- "database" : "_system",
- "url" : "/hello/world",
- "protocol" : "http",
- "server" : {
- "address" : "127.0.0.1",
- "port" : 37891
- },
- "client" : {
- "address" : "127.0.0.1",
- "port" : 44042,
- "id" : "156817587559043"
- },
- "internals" : {
- },
- "headers" : {
- "host" : "127.0.0.1",
- "content-length" : "16",
- "connection" : "Keep-Alive",
- "accept" : "application/json",
- "authorization" : "Basic cm9vdDo=",
- "content-type" : "application/json"
- },
- "requestType" : "PUT",
- "requestBody" : "{hello: 'world'}",
- "parameters" : {
- },
- "cookies" : {
- },
- "urlParameters" : {
- }
- },
- "options" : {
- }
- }
- shell> curl -X DELETE --header 'accept: application/json' --dump - http://localhost:8529/hello/world
- HTTP/1.1 Not Found
- content-type: application/json; charset=utf-8
- x-content-type-options: nosniff
- {
- "error" : true,
- "code" : 404,
- "errorNum" : 404,
- "errorMessage" : "unknown path '/hello/world'"
- }
Hide execution results
The former definition (defining url as an object with a match attribute)will result in the URL being accessible via all supported HTTP methods (e.g.GET, POST, PUT, DELETE, …), whereas the latter definition (providing a stringurl attribute) will result in the URL being accessible via HTTP GET andHTTP HEAD only, with all other HTTP methods being disabled. Calling a URLwith an unsupported or disabled HTTP method will result in an HTTP 404 error.