1

As DataSnap users will know, its ServerMethods return values to their callers as DataSnap parameters.

There have been a number of reports on SO and elsewhere relating to a problem with DataSnap servers returning ServerMethod results as ftStream parameters, that the stream is truncated prematurely or returned empty. An example is here: Can't retrieve TStreams bigger than around 260.000 bytes from a Datasnap Server

I have put together a reproducible test case of this that I intend submitting to Emba's Quality Portal as an MCVE, but before I do I'd like some help pinning down where the problem occurs. I'm using Delphi Seattle on Win64, compiling to 32-bits, btw.

My MCVE is completely self-contained (i.e. includes both server and client) and does not depend on any database data. Its ServerMethods module contains a function (BuildString in the code below) which returns a string of a caller-specified length and two ServerMethods GetAsString and GetAsStream which return the result as parameters of types ftString and ftStream, respectively.

Its GetString method successfully returns a string of any requested length up to the maximum I've tested, which is 32000000 (32 million) bytes.

Otoh, the GetStream method works up to a requested size of 30716; above that, the returned stream has a size of -1 and is empty. The expected behaviour of course that it should be capable of working with much larger sizes, just as GetString does.

On the outbound (server) side, at some point the returned stream is passed into DataSnap's JSon layer en route to the tcp/ip transport layer and on the inbound side, similarly, the stream is retrieved from the JSon layer. What I'd like to be able to do, and what this q is about, is to capture the outbound and inbound JSon representations of the AsStream parameter value in human-legible form so that I identify whether the unwanted truncation of its data occurs on the server or client side. How do I do that?

the reason I'm asking this is that despite hours of looking I've been unable to identify exactly where the JSon conversions occur. It's like looking for a needle in a haystack. If you take a look at the method TDBXJSonStreamWriter.WriteParameter in Data.DBXStream, the one thing it doesn't write is the stream's contents!

One thing I have been able to establish is regarding line 4809 in Data.DBXStream

Size := ((FBuf[IncrAfter(FOff)] and 255) shl 8) or (FBuf[IncrAfter(FOff)] and 255)

in the function TDBXRowBuffer.ReadReaderBlobSize. On entry to this method, Size is initialised to zero, and it is this line which sets Size to 30716 for all requested stream sizes >= that value. But I don't know whether this is cause or effect, i.e. whether the stream trucation has already taken place or whether it's this line which causes it.

My code is below; apologies for the length of it, but DataSnap projects require quite a lot of baggage at the best of times and I've included some code which initialises some of the components to avoid having to post .DFMs too.

ServerMethods code:

unit ServerMethods2u;
interface
uses System.SysUtils, System.Classes, System.Json, variants, Windows,
    Datasnap.DSServer, Datasnap.DSAuth, DataSnap.DSProviderDataModuleAdapter;

{$MethodInfo on}
type
  TServerMethods1 = class(TDSServerModule)
  public
    function GetStream(Len: Integer): TStream;
    function GetString(Len: Integer): String;
  end;
{$MethodInfo off}

implementation

{$R *.dfm}

uses System.StrUtils;

function BuildString(Len : Integer) : String;
var
  S : String;
  Count,
  LeftToWrite : Integer;
const
  scBlock = '%8d bytes'#13#10;
begin
  LeftToWrite := Len;
  Count := 1;
  while Count <= Len do begin
    S := Format(scBlock, [Count]);
    if LeftToWrite >= Length(S) then
    else
      S := Copy(S, 1, LeftToWrite);
    Result := Result + S;
    Inc(Count, Length(S));
    Dec(LeftToWrite, Length(S));
  end;
  if Length(Result) > 0 then
    Result[Length(Result)] := '.'
end;

function TServerMethods1.GetStream(Len : Integer): TStream;
var
  SS : TStringStream;
begin
  SS := TStringStream.Create;
  SS.WriteString(BuildString(Len));
  SS.Position := 0;
  Result := SS;
end;

function TServerMethods1.GetString(Len : Integer): String;
begin
  Result := BuildString(Len);
end;

ServerContainer code:

unit ServerContainer2u;
interface
uses System.SysUtils, System.Classes, Datasnap.DSTCPServerTransport,
  Datasnap.DSServer, Datasnap.DSCommonServer, Datasnap.DSAuth, IPPeerServer,
  DataSnap.DSProviderDataModuleAdapter;

type
  TServerContainer1 = class(TDataModule)
    DSServer1: TDSServer;
    DSTCPServerTransport1: TDSTCPServerTransport;
    DSServerClass1: TDSServerClass;
    procedure DataModuleCreate(Sender: TObject);
    procedure DSServerClass1GetClass(DSServerClass: TDSServerClass;
      var PersistentClass: TPersistentClass);
  end;

var
  ServerContainer1: TServerContainer1;

implementation

{$R *.dfm}

uses ServerMethods2u;

procedure TServerContainer1.DataModuleCreate(Sender: TObject);
begin
  DSServerClass1.Server := DSServer1;
  DSTCPServerTransport1.Server := DSServer1;
end;

procedure TServerContainer1.DSServerClass1GetClass(
  DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
  PersistentClass := TServerMethods1;
end;

end.

ServerForm code:

unit ServerForm2u;
interface
uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.StdCtrls, DBXJSON, Data.DBXDataSnap, IPPeerClient,
  Data.DBXCommon, Data.FMTBcd, Data.DB, Data.SqlExpr, Data.DbxHTTPLayer,
  DataSnap.DSServer;

type
  TForm1 = class(TForm)
    btnGetStream: TButton;
    edStreamSize: TEdit;
    SQLConnection1: TSQLConnection;
    SMGetStream: TSqlServerMethod;
    Memo1: TMemo;
    Label1: TLabel;
    btnGetString: TButton;
    Label2: TLabel;
    edStringSize: TEdit;
    SMGetString: TSqlServerMethod;
    procedure FormCreate(Sender: TObject);
    procedure btnGetStreamClick(Sender: TObject);
    procedure btnGetStringClick(Sender: TObject);
  private
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin

  SqlConnection1.ConnectionData.Properties.Values['CommunicationProtocol'] := 'tcp/ip';
  SqlConnection1.ConnectionData.Properties.Values['BufferKBSize'] := '64';

  SMGetStream.Params.Clear;
  SMGetStream.Params.CreateParam(ftInteger, 'Len', ptInput);
  SMGetStream.Params.CreateParam(ftStream, 'Result', ptOutput);

  SMGetString.Params.Clear;
  SMGetString.Params.CreateParam(ftInteger, 'Len', ptInput);
  SMGetString.Params.CreateParam(ftString, 'Result', ptOutput);

end;

procedure TForm1.btnGetStreamClick(Sender: TObject);
var
  SS : TStringStream;
  S : TStream;
begin
  Memo1.Lines.Clear;
  SS := TStringStream.Create;
  try
    SMGetStream.Params[0].AsInteger := StrtoInt(edStreamSize.Text);
    SMGetStream.ExecuteMethod;
    S := SMGetStream.Params[1].AsStream;
    S.Position := 0;
    if S.Size > 0 then begin
      try
        SS.CopyFrom(S, S.Size);
        Memo1.Lines.BeginUpdate;
        Memo1.Lines.Text := SS.DataString;
        Memo1.Lines.Insert(0, IntToStr(S.Size));
      finally
        Memo1.Lines.EndUpdate;
      end;
    end
    else
      ShowMessage(IntToStr(S.Size));
  finally
    SS.Free;
  end;
end;

procedure TForm1.btnGetStringClick(Sender: TObject);
var
  S : String;
  Size : Integer;
begin
  Memo1.Lines.Clear;
  Size :=  StrtoInt(edStringSize.Text);
  SMGetString.Params[0].AsInteger := Size;
  SMGetString.ExecuteMethod;
  S := SMGetString.Params[1].AsString;
  if Length(S) > 0 then begin
    try
      Memo1.Lines.BeginUpdate;
      Memo1.Lines.Text := S;
      Memo1.Lines.Insert(0, IntToStr(Length(S)));
    finally
      Memo1.Lines.EndUpdate;
    end;
  end;
end;

end.
Community
  • 1
  • 1
MartynA
  • 30,454
  • 4
  • 32
  • 73
  • See this answer. http://stackoverflow.com/a/1779710/544071 I would suggest this is as designed. – Jason Feb 22 '17 at 23:01
  • @Jason It has changed on Delphi 10.1 Berlin. On that answer and Martyn post they say that when a Stream exceeds 32Kb/64kb the received Stream reports a size of -1. While on 10.1 the received Stream shows always the full size, it's just that beyond 255kb is filled by zeroes. – Marc Guillot Feb 23 '17 at 10:05
  • 1
    @MarcGuillot: Interestingly, I tried the method in the accepted answer to the q Jason linked, and that successfully returns a stream up to the 32M I've tested, without the zero-filling you mentioned. That answer uses a client-side generated proxy class for the server. I haven't yet managed to figure out why that works, but calling the server using a TSqlServer method as per my q does not. – MartynA Feb 23 '17 at 10:47

0 Answers0