HttpAPI in details

Server Channel

To add REST support to the Remoting SDK server two things need to be done:

  • A special dispatcher should be added to a server channel
  • One or more service methods needs to be marked as ones that should be exposed via REST

Enabling the Http API support is as simple as instantiating a corresponding dispatcher and attaching it to a server channel:

HttpApiDispatcher dispatcher = new HttpApiDispatcher();
dispatcher.ApiHost = "localhost:8099";
dispatcher.Server = serverChannel as IHttpServer;
Dim dispatcher As HttpApiDispatcher = New HttpApiDispatcher
dispatcher.ApiHost = "localhost:8099"
dispatcher.Server = CType(serverChannel, IHttpServer)
var dispatcher: HttpApiDispatcher := new HttpApiDispatcher();
dispatcher.ApiHost := 'localhost:8099';
dispatcher.Server := IHttpServer(serverChannel);
let dispatcher = HttpApiDispatcher()
dispatcher.ApiHost = "localhost:8099"
dispatcher.Server = (serverChannel as? IHttpServer)
HttpApiDispatcher dispatcher = new HttpApiDispatcher();
dispatcher.ApiHost = "localhost:8099";
dispatcher.Server = (IHttpServer)serverChannel;
// dispatcher: TROHttpApiDispatcher;
dispatcher := TROHttpApiDispatcher.Create;
dispatcher.ApiHost := 'localhost:8099';
dispatcher.Server := ROServer;

The ApiHost property is descrubed below. Setting this property is not mandatory but it simplifies debug and use of auto-generated server access code later.

Only Http and SuperHttp channels can serve HttpAPI requests. It is even possible to use a separate Http server channel for HttpAPI while leaving more advanced SuperHttp for native Remoting SDK clients. This would allow faster response times and less resource consumption for HttpAPI requests.

The following properties might be optionally set if more customization is neded for the HttpAPI dispatcher:

  • Path - url path prefix that is processed by the given dispatcher. The default value is /api/. This means that requests like http://localhost:8099/api/... are processed by this Http dispatcher.
  • ApiHost - server hostname and port. The value of this property is used in the OpenAPI interface description.
  • ApiName - name of the API. The value of this property is used in the OpenAPI interface description.
  • ApiVersion - version of the API. The value of this property is used in the OpenAPI interface description.
  • AuthenticationManager - a reference to the authentication manager that performs authentication of requests to the Http API methods.

If the server application uses ApplicationServer class as a base class and loads its configuration from App.config file as described in Server Configuration via .config then the HttpAPI dispatcher might be configured via the following configuration Xml:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="ServerConfiguration" type="RemObjects.SDK.Server.ServerConfigurationSection,RemObjects.SDK.Server" />
  </configSections>
  <ServerConfiguration>
    <ServerChannel type="http" port="8100" />
    <ServerMessages>
      <ServerMessage type="bin" />
      <ServerMessage type="soap" />
    </ServerMessages>
    <SessionManager type="Memory" />
    <HttpApiDispatcher enabled="true" path="/api/" host="localhost:8099" name="Http API Sample Server" version="1.1.0.0" />
  </ServerConfiguration>
</configuration>

The Xml-set properties set by the HttpApiDispatcher section are similar to the ones that can be set via code.

In the code below it will be assumed that the dispatcher has been defined as

HttpApiDispatcher dispatcher = new HttpApiDispatcher();
dispatcher.Server = server.NetworkServer.ServerChannel as IHttpServer;
dispatcher.ApiHost = "localhost:8099";
Dim dispatcher As HttpApiDispatcher = New HttpApiDispatcher
dispatcher.Server = CType(server.NetworkServer.ServerChannel, IHttpServer)
dispatcher.ApiHost = "localhost:8099"
var dispatcher: HttpApiDispatcher := new HttpApiDispatcher();
dispatcher.ApiHost := 'localhost:8099';
dispatcher.Server := IHttpServer(server.NetworkServer.ServerChannel);
let dispatcher = HttpApiDispatcher()
dispatcher.ApiHost = "localhost:8099"
dispatcher.Server = (server.NetworkServer.ServerChannel as? IHttpServer)
HttpApiDispatcher dispatcher = new HttpApiDispatcher();
dispatcher.ApiHost = "localhost:8099";
dispatcher.Server = (IHttpServer)server.NetworkServer.ServerChannel;
// dispatcher: TROHttpApiDispatcher;
dispatcher := TROHttpApiDispatcher.Create;
dispatcher.ApiHost := 'localhost:8099';
dispatcher.Server := ROServer;

Service Methods

There are two approaches of defining services in Remoting SDK: CodeFirst and Classic RODL-based approach.

CodeFirst

To expose a service method via HttpApi mark it with the ApiMethod attribute:

[ApiMethod(HttpApiPath = "query/{someValue}", HttpApiMethod = ApiRequestMethod.Get, HttpApiResult = HttpStatusCode.OK, HttpApiTags = new String[] { "sometag", "someothertag" })]
public String SomeMethodExposedViaHttpApi(Int32 someValue, [ApiQueryParameter] String someString)
{
    return someValue.ToString() + someString;
}
<ApiMethod(HttpApiPath:="query/{someValue}", HttpApiMethod:=ApiRequestMethod.Get, HttpApiResult:=HttpStatusCode.OK, HttpApiTags:=New String() {"sometag", "someothertag"})>
Public Function SomeMethodExposedViaHttpApi(ByVal someValue As Int32, ByVal someString As String) As String
    Return (someValue.ToString + someString)
End Function
[ApiMethod(HttpApiPath := "query/{someValue}", HttpApiMethod := ApiRequestMethod.Get, HttpApiResult := HttpStatusCode.OK, HttpApiTags := [ "sometag", "someothertag" ])]
method SomeMethodExposedViaHttpApi(someValue: Int32; someString: String): String;
begin
  exit someValue.ToString() + someString;
end;
@ApiMethod(HttpApiPath = "query/{someValue}", HttpApiMethod =  ApiRequestMethod.Get, HttpApiResult = HttpStatusCode.OK, HttpApiTags = (["sometag", "someothertag"] as? String![]))
public func SomeMethodExposedViaHttpApi(_ someValue: Int32!, _ someString: String!) -> String! {
    return someValue.ToString() + someString
}
@ApiMethod(HttpApiPath = "query/{someValue}", HttpApiMethod = ApiRequestMethod.Get, HttpApiResult = HttpStatusCode.OK, HttpApiTags = new String[] { "sometag", "someothertag" })
public String SomeMethodExposedViaHttpApi(Int32 someValue, @ApiQueryParameter String someString)
{
    return someValue.ToString() + someString;
}
    [ROServiceMethod]
    [ROCustom('HttpApiPath', 'query/{someValue}')]
    [ROCustom('HttpApiMethod', 'GET')]
    [ROCustom('HttpApiResult', '200')]
    [ROCustom('HttpApiTags', 'sometag,someothertag')]
    [ROCustom('HttpApiOperationId', 'method')]
    function SomeMethodExposedViaHttpApi(
        someValue: Integer; 
        [ROCustom('HttpApiQueryParameter', '1')]someString: UnicodeString): UnicodeString;
...

function TMyService.SomeMethodExposedViaHttpApi(someValue: Integer; someString: UnicodeString): UnicodeString;
begin
  Result := IntToStr(someValue) + someString;
end;

All parameters set via this attribute are optional except of the HttpApiPath parameter. Let's take a closer look at the values set via this attribute:

  • HttpApiPath - the url template that can be used to call the service method. The path parts in the curly braces {} are bindings of the method parameters. When the method is called the corresponding part of the Url is extracted and provided as the service method parameter value.

Note: Only parameters of string, number, boolean, enumeration type can be bound via api method path.

  • HttpApiMethod - HTTP method that will be used to call the service. By default the POST method is used.
  • HttpApiRequestName - Name of the wrapper method parameters structure. By default server metainformation describes method parameters as a structure named {ServiceName}{MethodName}Request (f.e. LoginServiceLoginRequest). This optional parameter allows to override the auto-generated wrapper name. Redefined wrapper name should be unique server-wide.
  • HttpApiResult - HTTP response code that will be sent back to the client on successful method execution. By default the response will have the 200 OK Http code.
  • HttpApiTags - string array containing a list of tags that are applied to the service method. These tags are exposed via OpenAPI description file and are used by some codegens (f.e. the Swagger one) to group method proxies in the generated code.
  • HttpApiOperationId - custom operation name that can be used by the client code codegen instead of operation name that was auto-generated based on the API method path and Http method. For obvious reasons this operation name should be unique server-wide.

Note: Several service methods can share the same HttpApiPath value if they are bound to different HTTP methods.

The ApiMethodAttribute class is inherited from the ServiceMethodAttribute class so these notations are equal:

[ApiMethod(HttpApiPath = "test/{someValue}")]
public string DoSomething(String someValue)
<ApiMethod(HttpApiPath:="test/{someValue}")>  _
Public Function DoSomething(ByVal someValue As String) As String
[ApiMethod(HttpApiPath := 'test/{someValue}')]
method DoSomething(someValue: String);
@ApiMethod(HttpApiPath = "test/{someValue}"))
public func DoSomething(_ someValue: String!)
@ApiMethod(HttpApiPath = "test/{someValue}")
public String DoSomething(String someValue)

and

[ServiceMethod]
[ApiMethod(HttpApiPath = "test/{someValue}")]
public string DoSomething(String someValue)
<ServiceMethod(),  _
    ApiMethod(HttpApiPath:="test/{someValue}")>  _
Public Function DoSomething(ByVal someValue As String) As String
[ServiceMethod]
[ApiMethod(HttpApiPath := 'test/{someValue}')]
method DoSomething(someValue: String);
@ServiceMethod @ApiMethod(HttpApiPath = "test/{someValue}"))
public func DoSomething(_ someValue: String!)
@ServiceMethod
@ApiMethod(HttpApiPath = "test/{someValue}")
public String DoSomething(String someValue)

The ApiQueryParameter and ApiHeaderParameter attributes can be applied to the service method's parameters. These attributes state that the marked parameter should be passed as part of the Url query string or via Http headers accordingly. Only parameters of string, number, boolean, enumeration type can be sent via the Url query string.

Note: It is possible to pass one or more method parameter values via Http request headers. Such Http header should have the name matching to the parameter name prefixes with X- . F.e. if the parameter has name sessionId then Http header containing its value should have name X-SessionID or X-session-id. Header names are not case-sensitive.

RODL

As one might expect for classic RODL-based servers the HttpAPI based attributes have to be set via RODL.

The following custom attributes can be applied to RODL methods:

  • HttpApiPath
  • HttpApiMethod
  • HttpApiRequestName
  • HttpApiResult
  • HttpApiTags
  • HttpApiOperationId

These custom RODL attributes have exactly the same meaning as the corresponding attribute properties in a CodeFirst server.

However due to the text-only nature of custom RODL attribute values the HttpApiResult and HttpApiTags attributes are set in a slightly different way:

  • HttpApiResult value should be a numeric Http response code. F.e. the value of this attribute can be set to "200" or "201" but it cannot be set to "OK" or "Created".
  • HttpApiTags should be set to a comma-separated list of tags, f.e. "sometag,someothertag"

Also service method parameter can be marked with RODL attributes HttpApiQueryParameter or HttpApiHeaderParameter . If the value of this attribute is set to 1 then it is expected that the marked parameter will be passed via Url query string or Http headers accordingly (* see a note below).

Raw Method Result

This feature allows to send back method result AS IS, without wrapping it into a JSON object. For example a service method can generate an Excel report and send it back to the calling application, and the user's browser will immediately ask user to open or save the received document. At the same time the service method definition stays fully compatible with "conventional" Remoting SDK clients, so even existing methods can be amended with this feature without breaking the backwards compatibility. Let's assume there is a method that returns a Binary result. Calling this method via HttpAPI will result in a JSON object containing serialized Binary stream data, so additional code will be required client-side to properly process the received data. However instead of returning a RemObjects.SDK.Types.Binary instance the method might return a RemObjects.SDK.Types.ApiResult instance instead. Existing Remoting SDK feature (including RODL generator) will treat an ApiResult instance as a Binary instance, so no existing code will be broken by this substitution. At the same time the ApiResult type provides several Http API-specific features and properties:

  • HttpStatusCode - allows to set any possible HTTP response code (can be set only via constructor)
  • ContentType - allows to set a custom value of the response's Content-Type header. By default Http API responses have Content-Type header value set to application/json. This feature allows to set it to a custom value.
  • ContentIsInline and ContentFileName - allows to set the Content-Disposition header value. The ContentIsInline property controls whether the header value will contain inline or attachment prefix, while the optional ContentFileName property allows to set default file name that will be used by the user's browser to save the downloaded data.

Note: The RemObjects.SDK.Server.HttpApi.ApiResultContentType class contains a set of well-known Content-Type values like ones used for CSS, HTML, GIF, JPG, PNG data types.

For example let's take a look at this method:

[ServiceMethod]
[ApiMethod(HttpApiPath = "test")]
public Binary TestMethod()
{
    var data = File.ReadAllBytes(..image file location here..);
    return new Binary(data);
}
  [ROServiceMethod]
  [ROCustom('HttpApiPath', 'test')]
  function TestMethod(): ROAnsiString;
...
function TMyService.TestMethod(): Binary;
begin
  Result := TROBinaryMemoryStream.Create;
  Result.LoadFromFile(..image file location here..);
end;

Accessing Url like http://localhost:8099/api/test will result in a giant unreadable string containing some serialized data. Let's change the method result:

[ServiceMethod]
[ApiMethod(HttpApiPath = "test")]
public Binary TestMethod()
{
    var data = File.ReadAllBytes(..image file location here..);
    return new ApiResult(HttpStatusCode.OK,  ApiResultContentType.Jpg,"", true, data);
}
  [ROServiceMethod]
  [ROCustom('HttpApiPath', 'test')]
  function TestMethod(): Binary;
...
function TMyService.TestMethod(): Binary;
begin
  Result := TROHttpApiResult.Create(HTTP_200_code, id_ContentType_image_jpeg,'',false);
  Result.LoadFromFile(..image file location here..);
end;

Now the browser accessing the same Url will display an image instead a wall of text. And changing the method to

[ServiceMethod]
[ApiMethod(HttpApiPath = "test")]
public Binary TestMethod()
{
    var data = File.ReadAllBytes(..image file location here..);
    return new ApiResult(HttpStatusCode.OK,  ApiResultContentType.Jpg,"test.jpg", false, data);
}
  [ROServiceMethod]
  [ROCustom('HttpApiPath', 'test')]
  function TestMethod(): Binary;
...
function TMyService.TestMethod(): Binary;
begin
  Result := TROHttpApiResult.Create(HTTP_200_code, id_ContentType_image_jpeg,'test.jpg',false);
  Result.LoadFromFile(..image file location here..);
end;

will result in image being download instead of being displayed by the browser.

Error Reporting

The REST-ful paradigm implies that service methods should report exceptions occurred on the server side via Http error codes. By methods exposed via HttpAPI send these error codes:

  • 400 Bad Request - The request is misformed, f.e. request parameter values cannot be converted to data type expected by the service method
  • 401 Unauthorized - Client is not allowed to access the service method
  • 404 Not Found - The request Url path doesn't correspond to any API method path configured on the server
  • 500 Internal Server Error - An exception was raised by the service method
  • 501 Not Implemented - The request has either unsupported Http method or its Accept header has value other than application/json or * / *

It is possible to raise an error with Http code other than the generic 500 value using the ApiMethodException exception class.

Read more: