DataSnap REST Messaging Protocol

From RAD Studio
Jump to: navigation, search

Go Up to DataSnap REST


This document will describe the REST messaging protocol with DataSnap, which allows for remote invocation of server methods as well as the registering of heavyweight callbacks. With the details from this document you will be able to implement a client in any language that can communicate with a DataSnap server using HTTP Requests.

Remote Method Invocation

When a DataSnap server has an active component handling HTTP connections, a messaging protocol is exposed where HTTP requests can be issued to the server to invoke public methods of a class registered for the server with a TDSServerClass component. To invoke a server method, the most basic way would be with a GET request, with the following URL and no content in the message body:

http://host:port/datasnap/rest/[ClassName]/[MethodName]/[ParamValue]

Port is optional, as is ParamValue. 'datasnap' and 'rest' are also in a way optional, and may be required to be something else instead, depending on the values of the DSContext and RESTContext properties on the server for the HTTP connection.

Parameters in the URL

ParamValue represents a slash (/) separated list of parameter values, which match up with the input parameters required by the server method being invoked. If the server method (function/procedure) has no input parameters, then nothing should follow the method name in the URL. Two slashes (//) in the portion of the URL representing the parameters values set the value of the parameter at that index to be an empty string.

The values passed in this way must be encoded to be URL safe, which also includes encoding Unicode characters. Values with a JSON Object or JSON Array representation cannot be passed in this manner: they need to be passed in the body of the HTTP request, and the request type must be POST or PUT.

Parameters in the Request Body

If you want to invoke a server method that has an input parameter not supported by a GET or DELETE request (for example, one that cannot be represented as a string, number, Boolean, or null) or if you simply want to pass in one or more parameters through the content of the request, then you need to use a POST or PUT request type.

For example, if you want to invoke a server method such as this:

function TServerMethods1.updateEchoAttribute(Key: String; Obj: TJSONObject): String;

Then your request should look like this:

POST /datasnap/rest/TServerMethods1/EchoAttribute/Attr1 HTTP/1.1
....*additional headers*...
Accept: application/json
Content-Type: text/plain;charset=UTF-8

{"Attr1":"ValueToReturn"}

The above request contains a few items that require additional explanation. First of all, notice that the method name being passed in is "EchoAttribute" and not "updateEchoAttribute". This is because, by default, a prefix of 'update' is assigned to any method invoked with POST. Similarly, a prefix of 'cancel' is used for DELETE requests, and a prefix of 'accept' is used for PUT requests. This prefixing can be avoided by putting quotation marks around the method name, which would make the first line of the request look like this:

POST /datasnap/rest/TServerMethods1/%22updateEchoAttribute%22/Attr1 HTTP/1.1

This will also invoke the updateEchoAttribute server method, but the quotation marks prevent an additional 'update' from being prefixed to the given server name. This method name quoting also allows for invoking server methods with POST, PUT, or DELETE, which are not prefixed with the names matching the corresponding request type. For example, if the server method was instead:

function TServerMethods1.EchoAttribute(Key: String; Obj: TJSONObject): String;

Then method name quoting would be required, since the value of a TJSONObject is not supported in the request URL, meaning a GET request cannot be used, which is the only request type that does not assume a method prefix by default. The post would look like this:

POST /datasnap/rest/TServerMethods1/%22EchoAttribute%22/Attr1 HTTP/1.1

All of these examples have the same result, where the server method is invoked with "Attr1" as the value of the Key parameter, and the value of Obj will be a JSON Object that has a property called "Attr1" and a corresponding value. The content of the response will contain the result of invoking the server method, which in this example is implied to then be "ValueToReturn", as a string.

If you want to pass multiple parameter values in the request content, then you need to put them in a JSON array in the correct order, and make that array be the value of a JSON object pair, with the key "_parameters". The resulting JSON object should be represented as a string and set as the content of the request, looking like this:

{"_parameters":["Param1", "Param2"]}

When using this format, the input parameters will first be taken from the request URL, and then the remaining input values will be taken, in order, from the _parameters array.

Any query parameter is allowed in the request. The user will then have access from server methods to any of the query parameters passed in to the URL, by getting the Invocation Metadata (through a call to GetInvocationMetadata(), after adding the DBXPlatform to the uses clause) and accessing the list of parameters stored there. The server's HTTP service now has a FormatResult event, which allows it to pass back the JSON response in any format you want for specific method invocations.

Invocation Response

The response returned from an invocation request will have a response text that is a string representation of a JSON Object, looking something like this:

{"result":["ValueToReturn"]}

When parsed correctly into a JSON Object instance, you will be able to get the JSON Array for the 'result' property. This array contains the values for any parameters, in order of their placement in the server method's signature, which are returned (out, var, result types). The result is always the last item in the array, and in the above example is a JSON String with the value as it would be returned by the implementation of the EchoAttribute server method.

If an error occurs on the server during invocation, such as an expired session, unauthorized user, or invalid input values, then instead of a 'result' property, the returned JSON Object will contain an 'error' or 'SessionExpired' property, such the following:

{"SessionExpired":"The specified session has expired due to inactivity or was an invalid Session ID"}

Specifying the Stream Response Type

If the result of a method invocation is a TStream, then the stream can be either returned as a JSON Array or as the content stream of the response. To specify which to return, you can use the 'json' URL parameter, like this:

http://host:port/datasnap/rest/[Class]/[Method]/?json=true

You can set the value to either 'True' or 'False', where 'True' will have the result always returned as a JSON Array, and 'False' will return the result as a content stream, as long as no other var/out/result parameters are also being returned.

Authentication

If the server requires authentication, then you will need to pass in your authentication information (user name and password) properly formatted in the 'Authorization' header in your request. This is only required, however, when you are not specifying a session ID in the request. The format of the value for the Authorization header is as follows:

Basic base64(user:password)

Or for example, assuming the user name is 'admin' and the password is 'admin':

Basic YWRtaW46YWRtaW4=

Note that the string to base64-encode needs to be a single string representing the name and the password, with a colon between them.

Request Filters

Within DataSnap, there is the concept of filtering results using request filters. These predefined filters connect into an API, which allows you to specify in the URL which filter to use for each parameter and what values to pass into the specific filter. For example, a server function may return a very large string, but you might only ever need the first few characters of it. For this you can use the SubString filter, specifying the range of the string you are interested in. Here is an example of how the URL would look for a server function with a single input parameter and a string result:

http://host:port/datasnap/rest/[Class]/[Method]/[ParamValue]?ss.r=1,3

The part after the '?' defines which filter to use and on which parameters. In this case, the range function of the SubString filter is being used on the result being returned (if no parameter index is specified, the filter is applied on the result), specifying that the result should be a substring of three characters, starting at the second character of the result.

For more information, see Request Filters.

Session Information

When invoking a server method if you have not passed in a session ID in the request, then a new session will be created on the server and the session ID will be provided to you in the 'Pragma' header. The value stored for this header will contain two keys, 'dssession' and 'dssessionexpires'. The first is the ID of the session on the server, while the other is a close approximation for how many milliseconds remain before the session will expire. If 'dssessionexpires' is not provided or is a negative value, then the session will not expire, and should be closed when no longer needed by the client. Once you have the session ID, you must pass it in the 'Pragma' header of each additional request, with the value specified being in the format:

dssession=[session ID]

With the milliseconds until session expiry you can determine whether the session ID is no longer valid, and opt to not pass it in with a future request, since it would probably be invalid and return a SessionExpired error. If at any time you receive a SessionExpired result from the server, you should clear the session ID you have stored, and allow the server to provide you with a new one on your next request to the server.

To close your session before it expires, perform a GET request with the following URL, making sure to also pass in the session ID in the Pragma header of the request, as mentioned above:

http://host:port/datasnap/rest/CloseSession/

This will terminate the session with the provided ID, and return the following response text if successful (if unsuccessful, the SessionExpired message will be returned):

{"result":[true]}

Session Parameter Caching

One use for the sessions on the server is to optionally store parameter values from previous invocations, so some of the results of a method invocation can be retrieved multiple times without invoking the method itself more than once. The parameters that can be stored in the cache are those that require a JSON Object or JSON Array representation, such as streams and user-defined types.

To specify that you want to use the cache, you need to set the 'Accept' header value in the request to "application/rest". When you invoke a server method that has, for example, a result of TJSONObject, then a new cache item will be created, which allows accessing of the resultant JSON object. The value returned from the invocation will look like this:

{"result":["0/0/0"],"cacheId":0,"cmdIndex":0}

The "0/0/0" is in the place of where the actual invocation result would normally be. It is the identifier and relative path to the specific item in the cache. The format of this relative path can be interpreted as:

[Cache Item ID]/[Command Index]/[Parameter Index]

Cache Item ID is the unique identifier for the method invocation you executed. Only executions that store results in the cache get unique identifiers. Command Index identifies which command of the execution the result is for. This is only nonzero if you ran a batch execution (multiple server methods at once.) Parameter Index is the index of the complex parameter to return. This is zero-based, and is based on the command's complex parameters, not all of the command's parameters.

The additional values of 'cacheId' and 'cmdIndex' are the same values that will appear as the first two numbers in "0/0/X". If there is more than one cached result from the method invocation, the 'result' array will contain more items, but they will all share the cacheId and cmdIndex values, as they are part of the same cached item, and are results from the same method invocation.

You use these relative paths to build a cache URL. The cache URL uses 'cache' (CacheContext) instead of 'rest' (RESTContext), and then is suffixed with the relative URL returned in the invocation result.

Cache URLs support GET and DELETE request types. When issuing a GET, the following URLs are available:

http://host:port/datasnap/cache

Returns a JSON object describing the contents of the cache

http://host:port/datasnap/cache/(CacheID)

Returns a JSON object describing the cache object with the given ID

http://host:port/datasnap/cache/(CacheID)/(CmdIndex)

Returns a JSON representation of the given command

http://host:port/datasnap/cache/(CacheID)/(CmdIndex)/(ParameterIndex)

The actual parameter, as JSON or stream

When issuing a DELETE, the following URLs are supported:

http://host:port/datasnap/cache Delete all cached items
http://host:port/datasnap/cache/(CacheID) Deletes the cached item and all of its commands/parameters

If you do not clear the cache, it will be cleared and destroyed when the session expires. For more information, see DBX Parameter Caching.

Heavyweight Callbacks

REST heavyweight callbacks allow a client to issue a long-running HTTP request that acts as a callback registered with the server. The request has information associated with it, so that the server knows how to respond. When a response is retrieved, a new request is issued to the server to put the 'callback' back into a listening state.

Registering the Heavyweight Callback (Client Channel)

The original request, which begins the heavyweight callback handshake, should be formatted like the following and issued as a GET request:

http://host:port/datasnap/rest/DSAdmin/ConsumeClientChannel/[SCN]/[CHID]/[CBID]/[SCNS]/[ST]//

This handshake actually is registering a callback which itself contains one or more callbacks that it delegates into. SCN is the server channel name this callback should be listening to. This value can be anything that the server could potentially broadcast or notify to. SCNS is a comma-delimited list of server channel names for the registered callback. This can be an empty list (the callback will just use the channel's ServerChannelName) or a list that will add on to the channel's ServerChannelName. See the docwiki documentation on REST heavyweight callbacks for more information on this. As an example though, if the server was broadcasting like this:

Val := TJSONString.Create(Memo1.Text);
[DSServer].BroadcastMessage('MemoChannel', Val);

Then you would want the value of SCN passed in to be 'MemoChannel', so you could listen for that specific broadcast.

CHID is a unique ID to use for the client's callback channel. This can be anything, but the invocation will fail if it is not unique to the values already existing on the server. CBID is a unique ID for the first callback of the client's callback channel. Since a callback channel needs at least one callback in it at all times, this parameter is required. Additional child callbacks can be registered later. ST is a security token you define here. In all future calls that you issue to modify the client channel, such as adding or removing callbacks, you will need to specify this security token to validate you own the channel you are attempting to modify. The last parameter (//) is passed as an empty string when registering the channel.

If the registration was created successfully, a response is returned right away, looking like this:

{"result":[{"invoke":["cb12345",{"created":true},1]}]}

Where 'cb12345' is the unique ID of the callback registered with the creation of the client channel (CBID). Be sure to issue another request right away (see steps below on how to do this,) to keep the callback active.

Stopping the Heavyweight Callback

If you want to stop a previously created heavyweight callback, issue a GET request to the following URL:

http://host:port/datasnap/rest/DSAdmin/CloseClientChannel/[SCN]/[CHID]/[ST]

Where SCN, CHID, and ST are the same values passed in during initial creation.

Adding a Callback to a Client Channel

A client channel can contain multiple nested callbacks, but must contain at least one. To register another callback with the client channel, issue the following URL with a GET request:

http://host:port/datasnap/rest/DSAdmin/RegisterClientCallbackServer/[SCN]/[CHID]/[CBID]/[SCNS]/[ST]/

Where SCN, CHID, SCNS, and ST are the same values you used when creating the client channel, and CBID is an ID unique to the client channel for the new callback.

Removing a Callback From a Client Channel

You can remove callbacks from a client channel. If you remove the last callback, then the channel is closed. To remove a callback, issue the following URL with a GET request:

http://host:port/datasnap/rest/DSAdmin/UnregisterClientCallback/[SCN]/[CHID]/[CBID]/[SCNS]/[ST]/

Where SCN, CHID, SBID, SCNS, and ST are the same values passed in when registering the callback (either through the ConsumeClientChannel or RegisterClientCallbackServer method.)

Getting a Response From the Server

There are several situations when a response will come back to the heavyweight callback request. One case is as mentioned earlier, when the client channel is successfully created. Another is if the heavyweight callback is closed. The most common response, however, will be a broadcast or a notification. Notifications from a server target a single callback of a client channel, while broadcasts should be given to all callbacks.

If you have a callback with ID 'cd12345' then it is possible the response text of an issued request could look like this:

{"result":[{"invoke":["cb12345","Hello World" ,1]}]}

Which is saying that an invocation (notify) has been sent from the server, saying to invoke the callback with ID "cd12345" and pass in the string value "Hello World", which is type 1. Type 1 means the value should be treated as JSON, while type 2 means it is a JSON Object representation of a user object. Type 1 will probably be more common. The value does not need to be a string, it can be any JSON value.

If a broadcast is sent from the server, then it is directed at all of the callbacks of the client channel and so will not specify an ID. The response text would look like this:

{"result":[{"broadcast":["Hello World",1]}]}

If the heavyweight callback is closed, the following response text will be returned:

{"result":[{"close":true}]}

Responding to a Response From the Server

When your heavyweight callback request receives a response from the server, you must handle the message as quickly as possible and then issue a new request. The request you issue will be almost identical to the request issued for registering the client channel, with only a few small differences. First is that the command must be issued as a POST. This is what the request URL should look like:

http://host:port/datasnap/rest/DSAdmin/ConsumeClientChannel/[SCN]/[CHID]//[ST]

CBID is left as an empty string (since no new callbacks are being registered) and since it is a POST, the last parameter (which was an empty string by using two slashes for registering the client channel) is left out, because it will be passed in with the request body.

The request body must contain the result returned by the client channel, which can be any JSON value, in string format. In general, it is a good idea to return either 'True' or 'False'. If a notification was done, then the result should be the value returned for the specific callback being invoked. If a broadcast is done, the value returned should be based on all of the values returned by the callbacks (such as 'False' unless all callbacks returned 'True'.)

For more information, see REST Heavyweight Callbacks.

A PHP DataSnap REST Messaging Example

Following is an example that uses a native PHP code to consume a DataSnap REST that passes an array.

 <?php
 // type   -> unit name (uCidade) . object name (TCidade)
 // fileds -> fields must be the same in declared class
 $cidade = array( "type" => "uCidade.TCidade" , "id" => 1 , "fields" => array ( "FId" => 41000 , "FDescricao" => "LINS" , "FUF" => "SP" ) );
 $url  = 'http://localhost:8080/datasnap/rest/TServerMethodsMain/%22AddCidade%22/' ;
 $ch = curl_init() ;
 curl_setopt( $ch , CURLOPT_HTTPHEADER, array ( "Accept: application/json" , "Content-Type: text/xml; charset=utf-8" ) ) ;
 curl_setopt( $ch , CURLOPT_HEADER , FALSE ) ;
 curl_setopt( $ch , CURLOPT_RETURNTRANSFER , true ) ;
 curl_setopt( $ch , CURLOPT_POST , TRUE ) ;
 curl_setopt( $ch , CURLOPT_URL , $url ) ;
 curl_setopt( $ch , CURLOPT_POSTFIELDS , json_encode( $cidade ) ) ;
 $result = curl_exec( $ch ) ;

 echo '<pre>';
 print_r ($result);
 echo '</pre>';
 ?>

Note: RAD PHP has its own proxy generator but it would not generate a TJSONObject, as illustrated in the example above.