Web Server

General

{
  hello: "world",
  serverInformation: {
    serverName: "actionhero API",
    apiVersion: 1,
    requestDuration: 14
  },
  requestorInformation: {
    remoteAddress: "127.0.0.1",
    RequestsRemaining: 989,
    recievedParams: {
      action: ""
    }
  }
}
  

The web server exposes actions and files over http or https. You can visit the API in a browser, Curl, etc. {url}?action=actionName or {url}/api/{actionName} is how you would access an action. For example, using the default ports in /config/servers/web.js you could reach the status action with both http://127.0.0.1:8080/status or http://127.0.0.1:8080/?action=status

HTTP responses are always JSON and follow the format below.

HTTP Example

> curl 'localhost:8080/api/status' -v | python -mjson.tool
* About to connect() to localhost port 8080 (#0)
*   Trying 127.0.0.1...
* connected
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/status HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< X-Powered-By: actionhero API
< Date: Sun, 29 Jul 2012 23:25:53 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
{ [data not shown]
100   741    0   741    0     0   177k      0 --:--:-- --:--:-- --:--:--  361k
* Connection #0 to host localhost left intact
* Closing connection #0
{
    "requestorInformation": {
        "recievedParams": {
            "action": "status",
        },
        "remoteAddress": "127.0.0.1"
    },
    "serverInformation": {
        "apiVersion": "3.0.0",
        "currentTime": 1343604353551,
        "requestDuration": 1,
        "serverName": "actionhero API"
    },
    "stats": {
        "cache": {
            "numberOfObjects": 0
        },
        "id": "10.0.1.12:8080:4443:5000",
        "memoryConsumption": 8421200,
        "peers": [
            "10.0.1.12:8080:4443:5000"
        ],
        "queue": {
            "queueLength": 0,
            "sleepingTasks": []
        },
        "socketServer": {
            "numberOfGlobalSocketRequests": 0,
            "numberOfLocalActiveSocketClients": 0,
            "numberOfLocalSocketRequests": 0
        },
        "uptimeSeconds": 34.163,
        "webServer": {
            "numberOfGlobalWebRequests": 5,
            "numberOfLocalWebRequests": 3
        },
        "webSocketServer": {
            "numberOfGlobalWebSocketRequests": 0,
            "numberOfLocalActiveWebSocketClients": 0
        }
    }
}
  
  • you can provide the ?callback=myFunc param to initiate a JSONp response which will wrap the returned JSON in your callback function. The mime type of the response will change from JSON to Javascript.
  • If everything went OK with your request, no error attribute will be set on the response, otherwise, you should see either a string or hash error response within your action
  • to build the response for "hello" above, the action would have set connection.response.hello = "world";

Config Settings

/config/servers/web.js contains the settings for the web server. The relevant options are:

config.servers = {
  "web" : {
    secure: false,                       // HTTP or HTTPS?
    serverOptions: {},                   // Passed to https.createServer if secure=ture. Should contain SSL certificates
    port: 8080,                          // Port or Socket
    bindIP: "0.0.0.0",                   // Which IP to listen on (use 0.0.0.0 for all)
    httpHeaders : {                      // Any additional headers you want ActionHero to respond with
      'Access-Control-Allow-Origin' : '*',
      'Access-Control-Allow-Methods': 'PUT, GET, POST, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type'
    },
    urlPathForActions : "api",           // Route that actions will be served from; secondary route against this route will be treated as actions, IE: /api/?action=test == /api/test/
    urlPathForFiles : "public",          // Route that static files will be served from; path (relitive to your project root) to server static content from
    rootEndpointType : "api",            // When visiting the root URL, should visitors see "api" or "file"? Visitors can always visit /api and /public as normal
    directoryFileType : "index.html",    // The default filetype to server when a user requests a directory
    flatFileCacheDuration : 60,          // The header which will be returned for all flat file served from /public; defined in seconds
    fingerprintOptions : {               // Settings for determining the id of an http(s) requset (browser-fingerprint)
      cookieKey: "sessionID",
      toSetCookie: true,
      onlyStaticElements: false
    },
    formOptions: {                       // Options to be applied to incomming file uplaods. More options and details at https://github.com/felixge/node-formidable
      uploadDir: "/tmp",
      keepExtensions: false,
      maxFieldsSize: 1024 * 1024 * 100
    },
    metadataOptions: {                   // Options to configure metadata in responses
      serverInformation: true,
      requestorInformation: true
    },
    // When true, returnErrorCodes will modify the response header for http(s) clients if connection.error is not null.
    // You can also set connection.rawConnection.responseHttpCode to specify a code per request.
    returnErrorCodes: true,
    // should this node server attempt to gzip responses if the client can accept them?
    // this will slow down the performance of ActionHero, and if you need this funcionality, it is recommended that you do this upstream with nginx or your load balancer
    compress: false,
    // options to pass to the query parser
    // learn more about the options @ https://github.com/hapijs/qs
    queryParseOptions: {},
    // when true, an ETAG Header will be provided with each requested static file for caching reasons
    enableEtag: true
  }
}
  

Note that if you wish to create a secure (https) server, you will be required to complete the serverOptions hash with at least a cert and a keyfile:

config.server.web.serverOptions: {
  key: fs.readFileSync('certs/server-key.pem'),
  cert: fs.readFileSync('certs/server-cert.pem')
}
  

The Connection Object

{ id: '3e55b464fd34708eba26f609f44481a120e094a8-a6dfb60b-9562-4cc0-9d92-bc6cc1b622ba',
  connectedAt: 1447554153233,
  type: 'web',
  rawConnection:
   {
     req: {},
     res: {},
     params: { query: {} },
     method: 'GET',
     cookies: {},
     responseHeaders: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
     responseHttpCode: 200,
     parsedURL:
      Url {},
  remotePort: 57259,
  remoteIP: '127.0.0.1',
  error: null,
  fingerprint: '3e55b464fd34708eba26f609f44481a120e094a8',
  rooms: [],
  params: { action: 'randomNumber', apiVersion: 1 },
  pendingActions: 1,
  totalActions: 1,
  messageCount: 0,
  canChat: false,
  sendMessage: [Function],
  sendFile: [Function]
}
  

when inspecting connection in actions from web client, a few additional elements are added for convenience:

  • connection.rawConnection.responseHeaders: array of headers which can be built up in the action. Headers will be made unique, and latest header will be used (except setting cookies)
  • connection.rawConnection.method: A string, GET, POST, etc
  • connection.rawConnection.cookies: Hash representation of the connection's cookies
  • connection.rawConnection.responseHttpCode: the status code to be rendered to the user. Defaults to 200
  • connection.type for a HTTP client is "web"
  • connection.rawConnection.params.body will contain un-filtered form data
  • connection.rawConnection.params.files will contain un-filtered form data
  • connection.extension. If are using a route to access an action, and the request path ends in a file extension (IE: server.com/action/option.jpg), the extension will be available. Depending on the server's options, this extension may also be used to modify the response mime-type by configuring matchExtensionMimeType within each action.

Of course, the generic connection attributes (connection.error, connection.params, etc) will be present.

Sending Files

data.connection.sendFile('/path/to/file.mp3');
data.toRender = false;
next();
  

ActionHero can also serve up flat files. ActionHero will not cache these files and each request to file will re-read the file from disk (like the nginx web server).

  • /public and /api are routes which expose the ‘directories' of those types. These top level path can be configured in /config/servers/web.js with api.config.servers.web.urlPathForActions and api.config.servers.web.urlPathForFiles.
  • the root of the web server "/" can be toggled to serve the content between /file or /api actions per your needs api.config.servers.web.rootEndpointType. The default is api.
  • ActionHero will serve up flat files (html, images, etc) as well from your ./public folder. This is accomplished via the ‘file' route as described above. http://{baseUrl}/public/{pathToFile} is equivalent to http://{baseUrl}?action=file&fileName={pathToFile} and http://{baseUrl}/file/{pathToFile}.
  • Errors will result in a 404 (file not found) with a message you can customize.
  • Proper mime-type headers will be set when possible via the mime package.

There are helpers you can use in your actions to send files:

See the file server page for more documentation

Routes

For web clients (http and https), you can define an optional RESTful mapping to help route requests to actions. If the client doesn't specify an action via a param, and the base route isn't a named action, the action will attempt to be discerned from this config/routes.js file.

This variables in play here are:

  • api.config.servers.web.urlPathForActions
  • api.config.servers.web.rootEndpointType
  • and of course the content of config/routes.js

Say you have an action called ‘status' (like in a freshly generated ActionHero project). Lets start with ActionHero's default config:

api.config.servers.web.urlPathForActions = api';
api.config.servers.web.urlPathForFiles = ‘public';
api.config.servers.web.rootEndpointType = file';
  

There are 3 ways a client can access actions via the web server.

  • no routing at all and use GET params: server.com/api?action=status
  • with ‘basic' routing, where the action's name will respond after the /api path: server.com/api/status
  • or you can modify this with routes. Say you want server.com/api/stuff/statusPage
exports.default = function(api) {
  return {
    get: [
      { path: /stuff/statusPage', action: ‘status' }
    ]
  };
}
  

If the api.config.servers.web.rootEndpointType is "file" which means that the routes you are making are active only under the /api path. If you wanted the route example to become server.com/stuff/statusPage, you would need to change api.config.servers.web.rootEndpointType to be ‘api'. Note that making this change doesn't stop server.com/api/stuff/statusPage from working as well, as you still have api.config.servers.web.urlPathForActions set to be ‘api', so both will continue to work.

For a route to match, all params must be satisfied. So, if you expect a route to provide api/:a/:b/:c and the request is only for api/:a/:c, the route won't match. This holds for any variable, including :apiVersion. If you want to match both with and without apiVersion, just define the rote 2x, IE:

exports.default = function(api) {
  return {
    all: [
      { path: "/cache/:key/:value",             action:  "cacheTest" },
      { path: "/:apiVersion/cache/:key/:value", action:  "cacheTest" },
    ]
  };
}
  

If you want to shut off access to your action at server.com/api/stuff/statusPage and only allow access via server.com/stuff/statusPage, you can disable api.config.servers.web.urlPathForActions by setting it equal to null (but keeping the api.config.servers.web.rootEndpointType equal to ‘api').

Routes will match the newest version of apiVersion. If you want to have a specific route match a specific version of an action, you can provide the apiVersion param in your route definitions:

exports.default = function(api) {
  return {
    get: [
      { path: "/myAction/old", action:  "myAction", apiVersion: 1 },
      { path: "/myAction/new", action:  "myAction", apiVersion: 2 },
    ]
  };
}
  

This would create both /api/myAction/old and /api/myAction/new, mapping to apiVersion 1 and 2 respectively.

In your actions and middleware, if a route was matched, you can see the details of the match by inspecting data.connection.matchedRoute which will include path and action.

Finally, you can toggle an option, matchTrailingPathParts, which allows the final segment of your route to absorb all trailing path parts in a matched variable.

post: [
  // yes match: site.com/api/123
  // no match: site.com/api/123/admin
  { path: '/login/:userId(.*)', action: 'login' }
],

post: [
  // yes match: site.com/api/123
  // yes match: site.com/api/123/admin
  { path: '/login/:userId(.*)', action: 'login', matchTrailingPathParts: true }
],
  

This also enables "catch all" routes, like:

get: [
  { path: :path(.*)', action: ‘catchAll', matchTrailingPathParts: true }
],
  

If you have a route with multiple variables defined and matchTrailingPathParts is true, only the final segment will match the trailing sections:

get: [
  // the route site.com/users/123/should/do/a/thing would become {userId: 123, path: ‘/should/do/a/thing'}
  { path: /users/:userId/:path(.*)', action: ‘catchAll', matchTrailingPathParts: true }
],
  

Note: In regular expressions used for routing, you cannot use the "/" character.

Handling Static Folders with Routes

If you want map a special public folder to a given route you can use the "dir" parameter in your "get" routes in the routes.js file:

get: [
  { path: /my/special/folder', dir: __dirname + ‘/…/public/my/special/folder', matchTrailingPathParts: true }
],
  

After mapping this route all files/folders within the mapped folder will be accessible on the route.

You have to map the specified public folder within the "dir" parameter, relative to the routes.js file or absolute. Make sure to set "matchTrailingPathParts" to "true", because when it is set to false, the route will never match when you request a file. (e.g.: site.com/my/special/folder/testfile.txt).

Hosts

ActionHero allows you to define a collection of host headers which this API server will allow access from. You can set these via `api.config.servers.web.allowedRequestHosts`. If the `Host` header of a client does not match one of those listed (protocol counts!), they will be redirected to the first one present.

You can also set `process.env.ALLOWED_HOSTS` which will be parsed as a comma-separated list of Hosts which will set `api.config.servers.web.allowedRequestHosts`

Route Notes

  • actions defined in params directly action=theAction or hitting the named URL for an action /api/theAction will never override RESTful routing
  • you can mix explicitly defined params with route-defined params. If there is an overlap, the route-defined params win
    • IE: /api/user/123?userId=456 => connection.userId = 123
  • routes defined with the "all" method will be duplicated to "get", "put", "post", and "delete"
  • use ":variable" to define "variable"
  • an undefined ":variable" will not match
    • IE: "/api/user/" will not match "/api/user/:userId"
    • You would need a second route in this case to match "/api/user"
  • routes are matched as defined top-down in routes.js
  • you can optionally define a regex match along with your route variable
    • IE: { path:"/game/:id(^[a-z]{0,10}$)", action: "gamehandler" }
    • be sure to double-escape when needed: { path: "/login/:userID(^\\d{3}$)", action: "login" }
  • The HTTP verbs which you can route against are: api.routes.verbs = ['head', 'get', 'post', 'put', 'patch', 'delete']
exports.default = function(api) {
  return {
    get: [
      { path: "/users", action: "usersList" }, // (GET) /api/users
      { path: "/search/:term/limit/:limit/offset/:offset", action: "search" }, // (GET) /api/search/car/limit/10/offset/100
    ],

    post: [
      { path: "/login/:userID(^\\d{3}$)", action: "login" } // (POST) /api/login/123
    ],

    all: [
      { path: "/user/:userID", action: "user" } // (*) / /api/user/123
    ]
  };
}
  

Params

Params provided by the user (GET, POST, etc for http and https servers, setParam for TCP clients, and passed to action calls from a web socket client) will be checked against a whitelist defined by your action (can be disabled in /config/servers/web.js). Variables defined in your actions by action.inputs will be added to your whitelist. Special params which the api will always accept are:

[
  file',
  ‘apiVersion',
  callback',
  ‘action',
]
  

Params are loaded in this order GET -> POST (normal) -> POST (multipart). This means that if you have {url}?key=getValue and you post a variable key=postValue as well, the postValue will be the one used. The only exception to this is if you use the URL method of defining your action. You can add arbitrary params to the whitelist by adding them to the api.postVariables array in your initializers.

File uploads from forms will also appear in connection.params, but will be an object with more information. That is, if you uploaded a file called "image", you would have connection.params.image.path, connection.params.image.name (original file name), and connection.params.image.type available to you.

A note on JSON payloads:

You can post BODY json paylaods to actionHero in the form of a hash or array.

Hash: curl -X POST -d '{"key":"something", "value":{"a":1, "b":2}}' http://localhost:8080/api/cacheTest. This will result in:

connection.params = {
  key: something'
  value: {
    a: 1,
    b: 2
  }
}
  

Array: curl -X POST -d '[{"key":"something", "value":{"a":1, "b":2}}]' http://localhost:8080/api/cacheTest. In this case, we set the array to the param key payload:

connection.params = {
  payload: [
    {
      key: something'
      value: {
        a: 1,
        b: 2
      }
    }
  ]
}
  

Uploading Files

ActionHero uses the formidable form parsing library. You can set options for it via api.config.commonWeb.formOptions. You can upload multiple files to an action and they will be available within connection.params as formidable response objects containing references to the original file name, where the uploaded file was stored temporarily, etc. Here's an example:

// actions/uploader.js

exports.action = {
  name: 'uploader',
  description: 'uploader',
  inputs: {
    file1: {required: true},
    file2: {required: false},
    key1: {required: false},
    key2: {required: false},
  },
  outputExample: null,
  run: function(api, data, next){
    console.log("\r\n\r\n")
    console.log(data.params);
    console.log("\r\n\r\n")
    next();
  }
};
  
<!-- public/uploader.html -->

<html>
    <head></head>
    <body>
        <form method="post" enctype="multipart/form-data" action="http://localhost:8080/api/uploader">
            <input type="file" name="file1" />
            <input type="file" name="file2" />
            <br><br>
            <input type='text' name="key1" />
            <input type='text' name="key2" />
            <br><br>
            <input type="submit" value="send" />
        </form>
    </body>
</html>
  
// what the params look like to an action

{ action: 'uploader',
  file1:
   { domain: null,
     _events: null,
     _maxListeners: 10,
     size: 5477608,
     path: '/app/actionhero/tmp/86b2aa018a9785e20b3f6cea95babcca',
     name: '1-02 Concentration Enhancing Menu Initialiser.mp3',
     type: 'audio/mp3',
     hash: false,
     lastModifiedDate: Wed Feb 13 2013 20:32:49 GMT-0800 (PST),
     _writeStream:
      { ... },
     length: [Getter],
     filename: [Getter],
     mime: [Getter] },
  file2:
   { domain: null,
     _events: null,
     _maxListeners: 10,
     size: 10439802,
     path: '/app/actionhero/tmp/6052010f1d75ceaeb9197a9a759124dc',
     name: '1-10 There She Is.mp3',
     type: 'audio/mp3',
     hash: false,
     lastModifiedDate: Wed Feb 13 2013 20:32:49 GMT-0800 (PST),
     _writeStream:
      { ... },
  key1: '123',
  key2: '456',
 }
  

Client Library

Although the actionheroClient client-side library is mostly for websockets, it can now be used to make http actions when not connected (and websocket clients will fall back to http actions when disconnected)

  <script src="/public/javascript/actionheroClient.js"></script>

  <script>
  var client = new ActionheroClient();
  client.action('cacheTest', {key: 'k', value: 'v'}, function(error, data){
     // do stuff
  });
  </script>
  

Note that we never called client.connect. More information can be found on the websocket server docs page.

WebSocket Server

Overview

ActionHero uses Primus for web sockets. The Primus project allows you to choose from many websocket backends, including ws, engine.io, socket.io, and more. Within ActionHero, web sockets are bound to the web server (either http or https).

ActionHero will generate the client-side javascript needed for you (based on the actionheroClient library, primus, and the underlying ws transport). This file is regenerated each time you boot the application.

Connection Details

<script src="/public/javascript/actionheroClient.js"></script>

<script>

  client = new ActionheroClient;

  client.on('connected',    function(){ console.log('connected!') })
  client.on('disconnected', function(){ console.log('disconnected :(') })

  client.on('error',        function(error){ console.log('error', error.stack) })
  client.on('reconnect',    function(){ console.log('reconnect') })
  client.on('reconnecting', function(){ console.log('reconnecting') })

  // this will log all messages send the client
  // client.on('message',      function(message){ console.log(message) })

  client.on('alert',        function(message){ alert(message) })
  client.on('api',          function(message){ alert(message) })

  client.on('welcome',      function(message){ appendMessage(message); })
  client.on('say',          function(message){ appendMessage(message); })

  client.connect(function(error, details){
    if(error != null){
      console.log(error);
    }else{
      client.roomAdd("defaultRoom");
      client.action('someAction', {key: 'k', value: 'v'}, function(error, data){
        // do stuff
      });
    }
  });

</script>
  

connection.type for a webSocket client is "webSocket". This type will not change regardless of if the client has fallen back to another protocol.

Data is always returned as JSON objects to the webSocket client.

An example web socket session might be the following:

You can also inspect client.state (‘connected', ‘disconnected', etc). The websocket client will attempt to re-connect automatically.

If you want to communicate with a websocket client outside of an action, you can call connection.send(message) on the server. In the client lib, the event message will be fired. So, client.on('message, function(m){ ... }). Be sure to add some descriptive content to the message you send from the sever (like perhaps {"type": 'message type'}) so you can route message types on the client.

Client Methods

Methods which the provided actionheroWebSocket object expose are:

client.connect(callback)

  • callback will contain (error, details)
  • details here is the same as the detailsView method

client.action(action, params, callback)

  • action is a string, like "login"
  • params is an object
  • callback will be passed response (and you can inspect response.error)

client.say(room, message, callback)

  • message is a string
  • may contain an error
  • note that you have to first join a room with roomAdd to chat within it of recieve events

client.detailsView(callback)

  • callback will be passed error, response
  • the first response from detailsView will also always be saved to client.details for later inspection
  • may contain an error

client.roomView(room, callback)

  • will return metadata about the room
  • may contain an error

client.roomAdd(room, callback)

  • room is a string
  • may contain an error

client.roomLeave(room, callback)

  • room is a string
  • may contain an error

client.file(callback)

  • see below for details

client.disconnect()

  • instantly sever the connection to the server

The contents of the file callback look like:

{
  content: "<h1>ActionHero</h1>\nI am a flat file being served to you via the API from ./public/simple.html<br />",
  context: "response",
  error: null,
  length: 101,
  messageCount: 3,
  mime: "text/html"
}
  

Client Events

client.on(‘connected', callback)

  • no event data

client.on(‘disconnected', callback)

  • no event data

client.on(‘error', function(error){ console.log(‘error', error.stack) })

  • this is fired when a general error is encountered (outside of an action or verb)
  • this may fire when a general server error occurs

client.on(‘reconnect', function(){ console.log(‘reconnect') })

  • fired when client has reconnected
  • this will indicate that details, connection.id and other server-generated settings may have changed

client.on(‘reconnecting', callback })

  • client is attempting to reconnect to server

client.on(‘message', function(message){ console.log(message) })

  • this is VERY noisy, and is fired on all messages from the server, regardless of context or callback

client.on(‘alert', function(message){ alert(message) })

  • fired when message recieved from the server's context is specifically ‘alert'

client.on(‘api', function(message){ alert(message) })

  • fired when message recieved from the server's context is unknown or from the server

client.on(‘welcome', function(message){ console.log(message); })

  • server's welcome message

client.on(‘say', function(message){ console.log(message); })

  • fired on all say messages from other clients in all rooms
  • message.room can be inspected

Linking websockets to Web Clients

ActionHero provides connection.fingerprint where available to help you link websocket connections to related web connections. While every connection will always have a unique connection.id, we attempt to build connection.fingerprint by checking the headers the websocket connection began with. If the cookie defined by api.config.servers.web.fingerprint.cookieKey is present, we will store its value on the websocket connection.

You can read more about using a value like connection.fingerprint in an authentication middleware or using it as a key for session information.

Options

enabled:          true,
// you can pass a FQDN here, or function to be called / window object to be inspected
clientUrl:        'window.location.origin',
// Directory to render client-side JS.
// Path should start with "/" and will be built starting from api.config..general.paths.public
clientJsPath:     'javascript/',
// the name of the client-side JS file to render.  Both `.js` and `.min.js` versions will be created
// do not include the file exension
// set to `null` to not render the client-side JS on boot
clientJsName:     'actionheroClient',
// should the server signal clients to not reconnect when the server is shutdown/reboot
destroyClientsOnShutdown: false,

// Primus Server Options:
server: {
  // authorization: null,
  // pathname:      '/primus',
  // parser:        'JSON',
  // transformer:   'ws',
  // plugin:        {},
  // timeout:       35000,
  // origins:       '*',
  // methods:       ['GET','HEAD','PUT','POST','DELETE','OPTIONS'],
  // credentials:   true,
  // maxAge:        '30 days',
  // headers:       false,
  // exposed:       false,
},

// Priumus Client Options:
client: {
  // reconnect:        {},
  // timeout:          10000,
  // ping:             25000,
  // pong:             10000,
  // strategy:         "online",
  // manual:           false,
  // websockets:       true,
  // network:          true,
  // transport:        {},
  // queueSize:        Infinity,
  },
  

You can create your client with options. Options for both the server and client are stored in /config/servers/websocket.js. Note there are 3 sections: ‘server', ‘client', and ‘generation':

Socket Server

Overview

> telnet localhost 5000

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

{"welcome":"Hello! Welcome to the actionhero api","room":"defaultRoom","context":"api"}
$ detailsView
{"status":"OK","context":"response","data":{"id":"2d68c389-521d-4dc6-b4f1-8292cd6cbde6","remoteIP":"127.0.0.1","remotePort":57393,"params":{},"connectedAt":1368918901456,"room":"defaultRoom","totalActions":0,"pendingActions":0},"messageCount":1}
randomNumber
{"randomNumber":0.4977603426668793,"context":"response","messageCount":2}
$ cacheTest
{"error":"Error: key is a required parameter for this action","context":"response","messageCount":3}
$ paramAdd key=myKey
{"status":"OK","context":"response","data":null,"messageCount":4}
$ paramAdd value=myValue
{"status":"OK","context":"response","data":null,"messageCount":5}
$ paramsView
{"status":"OK","context":"response","data":{"action":"cacheTest","key":"myKey","value":"myValue"},"messageCount":6}
$ cacheTest
{"cacheTestResults":{"saveResp":true,"sizeResp":1,"loadResp":{"key":"cacheTest_myKey","value":"myValue","expireTimestamp":1368918936984,"createdAt":1368918931984,"readAt":1368918931995},"deleteResp":true},"context":"response","messageCount":7}
$ roomAdd default Room
{"status":"OK"}
$ say defaultRoom hooray!
{"status":"OK","context":"response","data":null,"messageCount":8}
  

You can access actionhero's methods via a persistent socket connection. The default port for this type of communication is 5000. As this is a persistent connection, socket connections have actionhero's verbs available to them. These verbs are:

  • quit disconnect from the session
  • paramAdd - save a singe variable to your connection. IE: ‘addParam screenName=evan'
  • paramView - returns the details of a single param. IE: ‘viewParam screenName'
  • paramDelete - deletes a single param. IE: ‘deleteParam screenName'
  • paramsView - returns a JSON object of all the params set to this connection
  • paramsDelete - deletes all params set to this session
  • roomAdd - connect to a room.
  • roomLeave - (room) leave the room you are connected to.
  • roomView - (room) show you the room you are connected to, and information about the members currently in that room.
  • detailsView - show you details about your connection, including your public ID.
  • say (room,) message

Please note that any verbs set using the above method will be ‘sticky' to the connection and sent for all subsequent requests. Be sure to delete or update your params before your next request.

To help sort out the potential stream of messages a socket user may receive, it is best to understand the "context" of the response. For example, by default all actions set a context of "response" indicating that the message being sent to the client is response to a request they sent (either an action or a chat action like say). Messages sent by a user via the ‘say' command have the context of user indicating they came form a user. Messages resulting from data sent to the api (like an action) will have the response context.

connection.type for a TCP/Socket client is "socket"

TLS

// TLS Config Options

config.severs.socket = {
  secure: false,                        // TCP or TLS?
  serverOptions: {},                    // passed to tls.createServer if secure=ture. Should contain SSL certificates
  port: 5000,                           // Port or Socket
  bindIP: "0.0.0.0",                    // which IP to listen on (use 0.0.0.0 for all)
};
  
config.server.socket.serverOptions: {
  key: fs.readFileSync('certs/server-key.pem'),
  cert: fs.readFileSync('certs/server-cert.pem')
}
  

You can switch your TCP server to use TLS encryption if you desire. Just toggle the settings in /config/servers/socket.js and provide valid certificates. You can test this with the openSSL client rather than telnet openssl s_client -connect 127.0.0.1:5000

Note that if you wish to create a secure (tls) server, you will be required to complete the serverOptions hash with at least a cert and a keyfile:

You can connect like: openssl s_client -connect 127.0.0.1:5000

or from node:

// Connecting over TLS from another node process

var tls = require('tls');
var fs = require('fs');

var options = {
  key: fs.readFileSync('certs/server-key.pem'),
  cert: fs.readFileSync('certs/server-cert.pem')
};

var cleartextStream = tls.connect(5000, options, function() {
  console.log('client connected', cleartextStream.authorized ? 'authorized' : 'unauthorized');
  process.stdin.pipe(cleartextStream);
  process.stdin.resume();
});
cleartextStream.setEncoding('utf8');
cleartextStream.on('data', function(data) {
  console.log(data);
});
  

Files and Routes

Connections over socket can also use the file action. There is no ‘route' for files.

  • errors are returned in the normal way {error: someError} when they exist.
  • a successful file transfer will return the raw file data in a single send(). There will be no headers set, not will the content be JSON.

JSON Params

The default method of using actions for TCP clients is to use the methods above to set params to their session and then call actions inline. However, you can also communication via JSON, passing along params specific to each request.

  • {"action": "myAction", "params": {"key": "value"}} is also a valid request over TCP

Client Suggestions

var actionheroClient = require("actionhero-client");
var client = new actionheroClient();

client.on("say", function(msgBlock){
  console.log(" > SAY: " + msgBlock.message + " | from: " + msgBlock.from);
});

client.on("welcome", function(msg){
  console.log("WELCOME: " + msg);
});

client.on("error", function(error, data){
  console.log("ERROR: " + error);
  if(data){ console.log(data); }
});

client.on("end", function(){
  console.log("Connection Ended");
});

client.on("timeout", function(error, request, caller){
  console.log(request + " timed out");
});

client.connect({
  host: "127.0.0.1",
  port: "5000",
}, function(){
  // get details about myself
  console.log(client.details);

  // try an action
  var params = { key: "mykey", value: "myValue" };
  client.actionWithParams("cacheTest", params, function(error, apiResponse, delta){
    console.log("cacheTest action response: " + apiResponse.cacheTestResults.saveResp);
    console.log(" ~ request duration: " + delta + "ms");
  });

  // join a chat room and talk
  client.roomAdd("defaultRoom", function(error){
    client.say("defaultRoom", "Hello from the actionheroClient");
    client.roomLeave("defaultRoom");
  });

  // leave
  setTimeout(function(){
    client.disconnect(function(){
      console.log("all done!");
    });
  }, 1000);

});
  

The main trick to working with TCP/wire connections directly is to remember that you can have many ‘pending' requests at the same time. Also, the order in which you receive responses back can be variable. if you request slowAction and then fastAction, it's fairly likely that you will get a response to fastAction first.

Note that only requests the client makes increment the messageCount, but broadcasts do not (the say command, etc)

The actionhero client library uses TCP/TLS connections, and makes use of actionhero's messageCount parameter to keep track of requests, and keeps response callbacks for actions in a pending queue. For example: