Is there a way to use JSONP with a Delphi DataSnap REST server?
Asked Answered
P

4

7

It appears that there is no way to implement a JSONP (JSON with Padding) solution using DataSnap, but I want to throw this question out here in case someone has solved this problem.

Background: JSONP is a mechanism that exploits the cross site referencing capability of the HTML script element to overcome the same origin policy of the XmlHttpRequest class. Using an XmlHttpRequest you can only obtain data (JSON objects) from the same domain that served the HTML document. But what if you want to retrieve data from multiple sites and bind that data to controls in the browser?

With JSONP, your src attribute of the script element does not reference a JavaScript file, but instead references a Web method (one that can reside on a different domain from which the HTML was retrieved). This Web method returns the JavaScript.

The script tag assumes that the returned data is a JavaScript file and executes it normally. However, what the Web method actually returns is a function call with a literal JSON object as its parameter. Assuming that the function that is called is defined, the function executes and can operate on the JSON object. For example, the function can extract data from the JSON object and bind that data to the current document.

The pros and cons of JSONP have been argued extensively (it represents a very serious security problem), so it is not necessary to repeat that here.

What I am interested in is if anybody out there has figured out how to use JSONP with Delphi's DataSnap REST servers. Here's the problem, as I see it. A typical JSONP usage may include a script tag that looks something like this:

<script type="application/javascript" src="http://someserver.com/getdata?callback=workit"> </script>

The getdata Web method would return a call something like the following:

workit({"id": "Delphi Pro", "price":999});

and the workit function might look something like this:

function workit(obj) {
  $("#namediv").val(obj.id);
  $("#pricediv").val(obj.price);
}

The issue is that DataSnap does not seem capable of returning a simple string like

workit({"id": "Delphi Pro", "price":999});

Instead, it is wrapped, like the following:

{"result":["workit({\"id\":\"Delphi Pro\",\"price\":999});"]}

Clearly this is not executable JavaScript.

Any ideas?

Prosthodontics answered 16/7, 2011 at 19:17 Comment(1)
the question can be reduced to "does DataSnap offer a filter/hook/event which allows to modify the generated JSON response before it is sent to the client"?Indusium
D
9

There is a way in Delphi DataSnap REST methods to bypass the custom JSON processing and return exactly the JSON you want. Here is a class function I use (in my Relax framework) to return plain data to a jqGrid:

class procedure TRlxjqGrid.SetPlainJsonResponse(jObj: TJSONObject);
begin
  GetInvocationMetadata().ResponseCode := 200;
  GetInvocationMetadata().ResponseContent := jObj.ToString;
end;

Info at http://blogs.embarcadero.com/mathewd/2011/01/18/invocation-metadata/.
Info at https://mathewdelong.wordpress.com/2011/01/18/invocation-metadata/.

BTW, you can assign nil to the result of the REST function.

Diuresis answered 18/7, 2011 at 9:47 Comment(1)
Thanks, Marco! Great solution. And thank you for including the link to Mat DeLong's blog about this technique.Prosthodontics
S
4

You can write a TDSHTTPServiceComponent descendant and hook it up with your instance of TDSHTTPService. In the following example an instance of TJsonpDispatcher is created at runtime (to avoid registering it in the IDE):

type
  TJsonpDispatcher = class(TDSHTTPServiceComponent)
  public
    procedure DoCommand(AContext: TDSHTTPContext; ARequestInfo: TDSHTTPRequest; AResponseInfo: TDSHTTPResponse;
      const ARequest: string; var AHandled: Boolean); override;
  end;

  TServerContainer = class(TDataModule)
    DSServer: TDSServer;
    DSHTTPService: TDSHTTPService;
    DSServerClass: TDSServerClass;
    procedure DSServerClassGetClass(DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
  protected
    JsonpDispatcher: TJsonpDispatcher;
    procedure Loaded; override;
  end;

implementation

procedure TServerContainer.DSServerClassGetClass(DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
  PersistentClass := ServerMethodsUnit.TServerMethods;
end;

procedure TServerContainer.Loaded;
begin
  inherited Loaded;
  JsonpDispatcher := TJsonpDispatcher.Create(Self);
  JsonpDispatcher.Service := DSHTTPService;
end;

procedure TJsonpDispatcher.DoCommand(AContext: TDSHTTPContext; ARequestInfo: TDSHTTPRequest;
  AResponseInfo: TDSHTTPResponse; const ARequest: string; var AHandled: Boolean);
begin
  // e.g. http://localhost:8080/getdata?callback=workit
  if SameText(ARequest, '/getdata') then
  begin
    AHandled := True;
    AResponseInfo.ContentText := Format('%s(%s);', [ARequestInfo.Params.Values['callback'], '{"id": "Delphi Pro", "price":999}']);
  end;
end;
Schafer answered 17/7, 2011 at 17:46 Comment(3)
Very nice solution. Thank you for your reply. I wish I could accept both your answer and Marco's answer as correct (not possible). I have accepted Marco's answer due to its simplicity. But, I have voted up your solution.Prosthodontics
@Cary Thank you! I would prefer Marco's answer, too, it's very interesting to learn about TDSInvocationMetadata. Unfortunately it doesn't seem to expose information about the request, only the response. Sometimes it might be useful to have access to the request data from within the server method.Schafer
Very nice solution, but in my code the DoCommand procedure is never called. Using Delphi XE7..Scope
M
3

the origin policy problem can be solved easyly in DataSnap. You can Customize the response header in this way:

procedure TWebModule2.WebModuleBeforeDispatch(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  **Response.SetCustomHeader('access-control-allow-origin','*');**
  if FServerFunctionInvokerAction <> nil then
    FServerFunctionInvokerAction.Enabled := AllowServerFunctionInvoker;
end;
Mclendon answered 14/9, 2011 at 16:33 Comment(1)
I have not had the opportunity to try this approach, but thank you for contributing.Prosthodontics
H
3

The answer from Nicolás Loaiza motivate me to find solution for TDSHTTPService, set customer response header in Trace event:

procedure TDataModule1.DSHTTPService1Trace(Sender:
    TObject; AContext: TDSHTTPContext; ARequest: TDSHTTPRequest; AResponse:
    TDSHTTPResponse);
begin
  if AResponse is TDSHTTPResponseIndy then
    (AResponse as TDSHTTPResponseIndy).ResponseInfo.CustomHeaders.AddValue('access-control-allow-origin', '*');
end;
Holsinger answered 29/6, 2012 at 3:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.