You are not logged in.
I'm posting this message to share with you my experience with ORM limitations in inserting or updating objects which are stored as virtual tables in Microsoft SQL Server and have one or more string properties encrypted using the Always Encrypted feature. Typical IRestORM Add or Update operations fail, because the corresponding parametrized queries send by SQLLite to the SQL server via ODBC, don't also set the parameters' column length, resulting in errors such as:
EOdbcException {Message:"TSqlDBOdbcStatement - TOdbcLib error: [22018] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
Operand type clash: varchar(max) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256',
column_encryption_key_name = 'CEK_Auto1', column_encryption_key_database_name = 'InsPumps') collation_name = 'Greek_CI_AS' is incompatible with
nvarchar(20) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256',
column_encryption_key_name = 'CEK_Auto1', column_encryption_key_database_name = 'InsPumps') (206)
[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Statement(s) could not be prepared.
This is a known MS SQL Server limitation with a workaround given in the following stack overflow post:
https://stackoverflow.com/questions/510 … ted-column
So in my case, I defined service methods to update or add such an object, which used ADO parametrized queries with size set for all the corresponding string properties.
Indeed @ab, you are right! Btw, the compiler is 32bit, Delphi XE.
Well, since the connected account is Admin and no update to the AccessRights property of the AuthGroup object was made before the Rest db update action, I did a debug on the client side, and found the following:
The TOrmAccessRights record (not an object, indeed), did not preserve the GET, PUT, POST, DELETE arrays between the call of the Edit method and a call of the toString method. To testify this, the following workaround code works fine!
var
grp: TAuthGroup;
oar: TOrmAccessRights;
ars: RawUTF8;
begin
grp := TAuthGroup.CreateAndFillPrepare(MainForm.RestClient.Orm, 'ID=?', [3]);
try
if grp.FillOne then
begin
oar := grp.SqlAccessRights;
oar.Edit(MainForm.RestClient.Model, TStatus, false, true, false, false);
oar.Edit(MainForm.RestClient.Model, TPerson, false, true, false, false);
FormatUtf8('%,%,%,%,%', [byte(oar.AllowRemoteExecute),
GetBitCsv(oar.GET, MAX_TABLES), GetBitCsv(oar.POST, MAX_TABLES),
GetBitCsv(oar.PUT, MAX_TABLES), GetBitCsv(oar.DELETE, MAX_TABLES)], ars);
grp.AccessRights := ars;
MainForm.RestClient.Update(grp);
end;
finally
grp.Free
end;
end;
Hi, the following code (when running it as Admin) executes with no exceptions, but the AccessRights field is not updated from its default value:
grp := TAuthGroup.CreateAndFillPrepare(MainForm.RestClient.Orm, 'ID=?', [3]);
try
if grp.FillOne then
begin
grp.SqlAccessRights.Edit(MainForm.RestClient.Model, TStatus,
false, true, false, false);
grp.SqlAccessRights.Edit(MainForm.RestClient.Model, TPerson,
false, true, false, false);
MainForm.RestClient.Update(grp);
end;
finally
grp.Free
end;
why?
TAuthUser has no PasswordPlain published property
That was exactly the meaning of my message @ab. The TAuthUser class has no published property with the plain password and I presume that it is meaningless to apply this validator on the PasswordHasHexa property. So, on which property can we apply this password validator? Can we create a dummy, DTO like class (not a TOrm descendant, or if it is mandatory to be a TOrm child to not include it in the model creation, so to avoid having this table in our DB), with a plain password as published property, then apply this validator on this property and if it succeeds to use this value to assign it to the PasswordPlain property of the TAuthUser class instance?
Not a quite elegant solution...
On which property of the TAuthUser class can we apply the TSynValidatePassWord validator? The following:
TAuthUser.AddFilterOrValidate('PasswordPlain', TSynValidatePassWord.Create())
raises an exception:
Project InsPumpSrv.exe raised exception class EOrmException with message 'TOrmPropInfoList.IndexByNameOrExcept(PasswordPlain): unkwnown field in TAuthUser'.
Hi, is there any way to get the Client (IRestORM) instance used in a TOrm class constructor, from within a method of this TOrm class?
Thanks Thomas for one more time. My approach at the end was to create a service which returns the result of the joined SQL query directly from the MS SQL server, bypassing SQLite:
function TInsPump.ListDoctors: RawUTF8;
var
st: TSqlDBOdbcStatement;
begin
Result := '';
st := TSqlDBOdbcStatement.Create(dbConn);
try
st.Prepare(StringToUtf8(c_SQL_List_Doctors), true);
st.ExecutePreparedAndFetchAllAsJson(false, Result);
finally
st.Free
end;
end;
At the client side, a TOrmTableJson is used to get the json and feed the TOrmTableToGrid with that:
restClient.Services['InsPump'].Get(insPump);
table := TOrmTableJson.Create('',insPump.ListDoctors);
table.OwnerMustFree := false;
OrmTableToGrid := TOrmTableToGrid.Create(DrawGrid1, table, restClient);
OrmTableToGrid.OnValueText := OnValueText;
A (final) question, because the documentation is not informant on this point: The first argument of the TOrmTableJson constructor, aSQL holds a comma separated list of column types? If yes, in what syntax, Delphi?
Thanks Thomas. I need to feed a TOrmTableToGrid object via the FillTable method, i.e. my goal is to fill a TOrmTable object with the TDoctor instances, with the joined objects expanded. If CreateAndFillPrepareJoined method worked as I expected, then it would be very easy, just by calling the FillTable method of the TOrm object. Now I have to find an alternative approach.
But, it is a many (TDoctor) to one (TLocArea) relation, not a m:n. So far I haven't seen in the documentation (either in 1.18 or in 2.0) that a TOrmMany base class is needed for such relationship.
I got two ORM classes joined, TDoctor and TLocArea:
TDoctor = class(TOrm)
private
fFirstName, fSurName: RawUTF8;
fLocationArea: TLocArea;
fMISId, fSAPId: TNullableUtf8Text;
published
property FirstName: RawUTF8 index 50 read fFirstName write fFirstName;
property SurName: RawUTF8 index 50 read fSurName write fSurName;
property LocationArea: TLocArea read fLocationArea write fLocationArea;
property MISId: TNullableUtf8Text index 20 read fMISId write fMISId;
property SAPId: TNullableUtf8Text index 20 read fSAPId write fSAPId;
end;
TLocArea = class(TOrm)
private
fArea, fNs: RawUTF8;
published
property Area: RawUTF8 index 50 read fArea write fArea;
property Ns: RawUTF8 index 10 read fNs write fNs;
end;
The actual tables are implemented in MS SQL Server, as virtual tables through SQLite. The connection is via ODBC. In the client application the data is retrieved using the CreateAndFillPrepareJoined method.
doctor := TDoctor.CreateAndFillPrepareJoined(restClient.Orm, '', [], []);
I am able to create some TLocArea objects and a TDoctor object, assign its properties and store it in the DB. I retrieve back the instance with CreateAndFillPrepareJoined with no problem. When I add a second TDoctor object it is persisted in the DB, however the CreateAndFillPrepareJoined returns an empty set, i.e. calling:
doctor.FillOne
returns false and no objects can be retrieved, although they are already stored in the DB tables. Activating logging in the mormot server traces the following related lines:
20230705 04572622 DB mormot.db.sql.odbc.TSqlDBOdbcStatement(02b02e50) Prepare t=10.59ms q=select FirstName,SurName,LocationArea,MISId,SAPId,ID from dbo.Doctor
20230705 04572622 SQL mormot.db.sql.odbc.TSqlDBOdbcStatement(02b02e50) Execute t=10.87ms q=
20230705 04572622 SQL mormot.orm.sql.TOrmVirtualTableCursorExternal(02ab0680) Search select FirstName,SurName,LocationArea,MISId,SAPId,ID from dbo.Doctor
20230705 04572622 DB mormot.db.sql.odbc.TSqlDBOdbcStatement(02b03030) Prepare t=59us q=select Area,Ns,ID from dbo.LocArea where ID=?
20230705 04572622 warn vt_Filter Search()
20230705 04572622 ERROR mormot.rest.sqlite3.TRestServerDB(023d4d40) {"ESqlite3Exception(02507d38)":{Message:"Error SQLITE_ERROR (1) [Step] using 3.41.0 - SQL logic error",ErrorCode:1,SQLite3ErrorCode:"secERROR"}} for SELECT Doctor.RowID as `Doctor.RowID`,Doctor.FirstName as `Doctor.FirstName`,Doctor.SurName as `Doctor.SurName`,Doctor.MISId as `Doctor.MISId`,Doctor.SAPId as `Doctor.SAPId`,LocationArea.RowID as `LocationArea.RowID`,LocationArea.Area as `LocationArea.Area`,LocationArea.Ns as `LocationArea.Ns` FROM Doctor LEFT JOIN LocArea AS LocationArea ON Doctor.LocationArea=LocationArea.RowID //
The profiler at the MS SQL server side traces only the first Prepare statement from the lines above.
Last, when I try to retrieve the TDoctor objects with no joins, e.g. by using the CreateAndFillPrepare method, I can get the two stored rows with no problem. Any help is very much appreciated.
Thanks, but the problem persists. So I presume that the above commit just documents the missing code and doesn't port it into mormot2?
Delphi
I have a TClientDataSet instance created via the ToClientDataSet function of the mormot.db.rad.ui.cds unit:
status := TStatus.CreateAndFillPrepare(restClient.Orm,'');
dsStatus.DataSet := ToClientDataSet(Self, status.FillTable, restClient);
dsStatus is a TDataSource component linked to a DBGrid on a form. How can I apply the updates in the grid back to database? The following has no effect:
if (dsStatus.DataSet is TClientDataSet) and dsStatus.DataSet.Active then
TClientDataSet(dsStatus.DataSet).ApplyUpdates(0);
You're welcome, thank you @ab (and the rest of the mormot contributors) for your outstanding work!
I found the problem's cause in mormot.ui.core unit, in procedure DrawTextUtf8. Utf8DecodeToUnicode doesn't seem to work properly. Below is the modified version which works in my system (the original code is commented out):
procedure DrawTextUtf8(Control: TCustomControl; Canvas: TCanvas;
const Text: RawUtf8; var Rect: TRect; CalcOnly: boolean);
var
// tmp: TSynTempBuffer;
s: string;
begin
// Utf8DecodeToUnicode(Text, tmp);
s := Utf8ToString(Text);
DrawText(Canvas.Handle, PWideChar(s), length(s), Rect,
DrawTextFlags(Control, CalcOnly));
// DrawText(Canvas.Handle, tmp.buf, tmp.len shr 1, Rect,
// DrawTextFlags(Control, CalcOnly));
// tmp.Done;
end;
In MS Windows 10, a win32 mormot app compiled with Delphi XE. The following displays the contents of a TOrm class (TStatus) in a DrawGrid, using the TOrmTabletoGrid class (table is a local TOrmTable variable):
if ConnectToServer then
begin
status := TStatus.CreateAndFillPrepare(Client.Orm, '');
try
table := status.FillTable;
table.OwnerMustFree := false;
OrmTableToGrid := TOrmTableToGrid.Create(DrawGrid1, status.FillTable, Client);
OrmTableToGrid.OnHintText := OnHintText;
finally
status.Free
end
end
ctrl-click displays hint strings polluted with erroneous characters, like Chinese, rectangles etc. However the debug of OnHintText event handler shows that the Text output variable contains the correct string values:
function TMainForm.OnHintText(Sender: TOrmTable; FieldIndex, RowIndex: Integer;
var Text: string): boolean;
begin
Text := (Sender as TOrmTable).GetString(RowIndex,FieldIndex);
Result := true
end;
Any help is welcome.
NullableDateTimeToValue fails to get the value of TNullableDateTime properties stored in the DB as ISO8601 strings, because the VariantToDouble internal call fails to convert the stored value into double.
A workaround is to use the functions NullableUtf8TextToValue in conjunction with the Iso8601ToDateTime, to convert the stored value into TDatetime.
Thanks Thomas!
Hi
I've noticed that calculated fields in ORM classes created as read-only published properties, are actually created as DB tables. For example in this class:
TPerson = class(TOrm)
private
fFirstName, fSurName: RawUTF8;
protected
function GetFullName: RawUTF8;
published
property FirstName: RawUTF8 index 50 read fFirstName write fFirstName;
property SurName: RawUTF8 index 50 read fSurName write fSurName;
property FullName: RawUTF8 read GetFullName;
end;
registered as a virtual MS-SQL Server table, ORM actually creates a field named FullName in the database table. Is there any purpose on this? In case I don't want a calculated field to be actually created as a db table field, do I have to use a public calculation function instead (e.g. GetFullName), and not a published property?
Hi,
I have a case where all model tables have to be created as virtual and reside to a Microsoft SQL Server. Unfortunately this has to be done via ODBC, because in some columns I have to enable the Always Encrypted feature.
So far I have a simple model with only TAuthUser and TAuthGroup defined. These have been registered with VirtualTableExternalRegister and with the corresponding TSqlDBOdbcConnectionProperties props object. The ODBC data source uses the version 17 of the Microsoft SQL Server ODBC driver. The RestServerDB is created as an in memory instance (with ':memory:').
The call of CreateMissingTables method raises the following non-blocking exception for each of the created tables (e.g. two times):
exception class EOdbcException with message 'TSqlDBOdbcStatement - TOdbcLib error: [HY010] [Microsoft][ODBC Driver Manager] Function sequence error (0)
Though it is a non-blocking exception, I post it here in case it might affect other SQL operations by the ORM classes.
@ab
I pulled the changes from the repository, built and run again the program. Now all tests pass successfully!
I confirm that this is a locale related problem. My current locale setting is el_GR.UTF-8. When I run the mormot2tests program inside a container (via DDEV-Local) which by default, has locale C.UTF-8 all tests are passed successfully.
Is it possible to have a locale agnostic static version of quickjs?
@ab
damian@ifestos:~/mORMot2/static/x86_64-linux> ll quickjs.o
-rw-r--r-- 1 damian users 1072632 Απρ 2 22:24 quickjs.o
My CPU is Intel Core i5-4460 @ 3.20GHz
Thanks a lot @ttomas for the hint. With this option I can debug the tests project. The failed test is at line 166 of test.core.script unit:
CheckEqual(Run(
'add(434.732,343.045)'), '777.777');
The evaluation of the javascript expression gives the following result:
<JSVALUE> = { U = { I32 = 777, TAG = 65536, F64 = 1.3906711615708398e-309, PTR = $1000000000309, U64 = 281474976711433}}
which is apparently integer and the equality check with the float value '777.777' fails.
With the debugging options defined in the Lazarus project, I can't debug it. I get the following GDB error:
The GDB command:
"-break-insert +0"
did not return any result.
I changed the build mode to lin64, rebuilt and run the project again. Now all tests are passed, except the 1000 quickjs related errors. All these are raised in section 3.1 (SOA/Core script).
I changed the debugging options but I'm still getting the same GDB error.
Lazarus (2.0.11)/FPC (3.2.0) installation is made via fpcupdeluxe.
UPDATE: I created another Lazarus project besides the mormot2tests and tried to re edit it from scratch. I kept the program body minimum (only a writeln statement) and I added progressively the uses section. Debugging was available and OK, till I added the test.core.base unit.
Hi
I cloned the mORMot2 repository, downloaded the static files with SQLite 3.35.5, compiled and run the regression tests, using FPC 3.2.0 on OpenSUSE Linux. Unfortunately, some tests failed:
/home/damian/mORMot2/test/fpc/bin/x86_64-linux/mormot2tests 0.0.0.0 (2021-07-07 16:36:50)
Host=XXXX User=damian CPU=4xIntel(R)Core(TM)i5-4460CPU@3.20GHz(x64):FFFBEBBFBFFBFA7FAB270000000000000006009C OS=Suse=Linux-5.3.18-lp152.78-default#1-SMP-Tue-Jun-1-14:53:21-UTC-2021-(556d823) Wow64=0 Freq=1000000
TSynLog 2.0.1 2021-07-07T13:38:23
0000000000000001 ! fail #5 ../../src/core/mormot.core.test.pas tsyntestcase.testfailed (944) ../../src/core/mormot.core.test.pas tsyntests.run (1171) mormot2tests.dpr main (96)
00000000000156FF ! fail #7 ../../src/core/mormot.core.test.pas tsyntestcase.testfailed (944) ../../src/core/mormot.core.test.pas tsyntests.run (1171) mormot2tests.dpr main (96)
0000000000016CF9 ! fail #9 ../../src/core/mormot.core.test.pas tsyntestcase.testfailed (944) ../../src/core/mormot.core.test.pas tsyntests.run (1171) mormot2tests.dpr main (96)
00000000027A9CFE ! fail #6046 777<>777.777 ../../src/core/mormot.core.test.pas tsyntestcase.testfailed (944) test.core.script.pas ttestcorescript.quickjslowlevel (170) ../../src/core/mormot.core.test.pas tsyntests.run (1171) mormot2tests.dpr main (96)
... (the error above is repeated 1000 times up to #40012)
Any hints?
I've noticed when running a TSynDaemon program with --console parameter on Linux that the method Stop is called twice when I press [Enter] to quit. This raised an AV Exception in method implementation as the following:
procedure TBareDaemon.Stop;
begin
fHttpServer.Free;
fServer.Free;
end;
and I had to rewrite it as follows:
procedure TBareDaemon.Stop;
begin
if fHttpServer <> nil then
begin
fHttpServer.Free;
fHttpServer := nil
end;
if fServer <> nil then
begin
fServer.Free;
fServer := nil
end;
end;
I got this AV Exception also in sample "37 - FishShop Service", for the same reason.
OK I see. However, I think that the token's renewal in the context of a valid request when it is close to expire is simpler (it acts also as an implicit renewal request), with no need to implement a separate renewal action from the client's side or worse, to put this action inside polling (e.g. like window.setInterval etc.), which is more error-prone and risks mishandling of idle user. The only consequence is a need for a blacklist (also recommended as a good practice by owasp), which is covered fine by a TSynDictonary property.
@ab, I don't put the JWT in the URI. The Login request uses POST method and the token is returned as the last item of the Ctxt.Results array, in the context of the POST response.
if bSuccess then
Ctxt.Results(['User ' + jData.username + ' logged in successfully', token])
else Ctxt.Error('Incorrect username or password', HTTP_UNAUTHORIZED);
At the client's side the token is stored in a global js variable or in the sessionStorage and it is included in the requests' header (as a Bearer).
Concerning the expiration, I think that as long as the client is active (e.g. sends requests to the server), it will be annoying to prompt for a login page. If the client is idle, then it is reasonable ask for a new login, since the token is expired.
You don't have a JWT any more, but a "classical" session token.
I could understand this argument if the token is actually stored in the cookie, but this is not the case here. The hardened cookie is used just to create a path outside javascript scope, so the server can cross check not only the token's signature but also the client's origin (sender). That is I think the reasoning of the OWASP recommendation.
On the client side, you may safely store the credentials (user/password) in memory using our CryptDataForCurrentUser() function, to avoid prompting the end-user.
This is suitable for a Delphi client, but in this case I'm talking about a javascript client (e.g. a single page js application). As far as the service receives valid requests I see no reasons to leave the token expire and prompt the web user to enter again the credentials, moreover since the timeout is kept quite short (15 minutes).
There is a SessionID session in mORMot. Just store the session ID within the JWT, and compare it with the token. When the client closes its session, the SessionID will be rejected.
The web client's session is between the js web app and the front desk in memory service which by default doesn't handle user's authentication (aHandleUserAuthentication is false). So I presume that any SessionID will be between the front desk service and another mORMot service which maintains the user credentials. It doesn't seem very useful to me to pass this SessionID to the web client.
Thanks @ab for your remarks.
Following the advice of ab to not expose a TSQLModel in a web service intended to publish a web api to a javascript application, I created a simple method based class to handle user authentication, using an abstract username/password check and jwt afterwards. This class is intended to be used as a base class to descent a front desk method based service to implement the actual web api. You can find the source code below:
https://gist.github.com/dadim/4443f9062 … 24a70d2077
The class TFronDeskMethodService descents from TSQLRestServerFullMemory. It has two published methods, Login and Logout. The first expects a POST method request with a json with two properties, username and password (plain). Login uses an abstract method, BackendLogin, which checks the user's credentials against a back end service (e.g. another mORMot service, an external provider etc.). If it is successful, computes and sends to the web client a jwt token as the last item of the results array. The secret of all computed tokens is a random 32 bytes long RawBytesString, created in every instance of this class.
To prevent sidejacking, I followed OWASP's recommendation to include in the token (as subject), a random string for each authenticated user. This random string is also sent to the user as a hardened cookie. The class uses also a TSynDictionary to store blacklisted token IDs, whose entries timeout after the elapsing of fJWTMinutesValid (by default 15 min).
Every other api method has to check the request in a similar way as the logout method does, e.g. by a combination of AuthenticationCheck, blacklist dictionary lookup and subject check. The token is renewed by CheckSubject method if it is called within the last 33% of the fJWTMinutesValid time.
You are welcomed to use, modify, distribute it or at least post comments or suggestions.
Thanks a lot!
As far as I've read in the documentation, post method requests are not supported in method based services. Am I correct?
I usually publish the services using a TSQLRestServerFullMemory instance with nothing but the SOA interfaces published in it - i.e. a void model with no TSQLRecord in it.
How do you handle user authentication/authorization at this level?
Please try https://synopse.info/fossil/info/aba10df87c
It works fine!
@damiand - try to replace in THttpSocket.GetBody
...
Does it help?
mpv I followed your advice and in line 6897 of SynCrtSock I made the following addition after the else:
if ContentLength>0 then begin
...
end else ContentLength := 0;
if ContentLength<0 then begin // ContentLength=-1 if no Content-Length
...
end;
And the problem solved! Do you think ab that this change should be applied also in the git repository? Many thanks.
Please try locally, directly on the mORMot server.
I've already tried it - as I wrote in my previous post - and there was no delay, that's why I concluded that the problem must be in the middle.
BTW you should better not publish the ORM remote REST access to the clients...
How can I prevent this publishing? I did nothing about it, that's the default way of operation. As far as I tested it - and as written in the documentation - only the Admin and Supervisor roles have access to these tables.
Thanks, ab.
My latest mORMot commit applied is 60aeaba63d770dcad2841aa0fe9e234d1982b51a from Feb 8 if I get it correctly (I'm not experienced with git). The whole installation is on OpenSUSE Linux Leap 15.2. The server is compiled with FPC 3.2.0. There is a reverse proxy indeed, because the client is an HTML page with jQuery, served by nginx hosted in a docker container, configured by ddev. The nginx is configured with TLS/SSL and as a reverse proxy to the mORMot server, located outside the container in the main operating system.
There is a similar behavior with Firefox and Chrome. A plain GET request to timestamp as you suggested takes 1ms to complete. Other CRU db actions to AuthUser using REST are performed rapidly. When I bypass the web server/docker container and load the html page from the filesystem, the delete is performed with no delay, provided that I have included also in the mORMot server compilation the AccessControlAllowOrigin := '*' setting. The latter is neither necessary in the previous (web hosted) setup, nor affects the delete performance. In conclusion, the delay resides in-between in the nginx reverse proxy, but I can't find out the reason yet.
When I send (as an Admin user) from a web application delete ajax requests to AuthUser the response is very slow, about 10 secs in a local development environment. The debug log shows no signs of execution delay:
20210224 13241602 # + mORMotSQLite3.TSQLRestServerDB(7ffff7f99590).URI DELETE root/AuthUser/11?session_signature=021c47f800d436046250611b in=0 B
20210224 13241602 # auth mORMot.TSQLRestRoutingREST(7ffff4c473d0) Admin/35407864 172.18.0.3
20210224 13241602 # cache SynSQLite3.TSQLDatabase(7ffff7fee220) bareserver.db3 cache flushed
20210224 13241602 # DB mORMotSQLite3.TSQLRestServerDB(7ffff7f99590) prepared 22us bareserver.db3 DELETE FROM AuthUser WHERE RowID=?;
20210224 13241602 # SQL mORMotSQLite3.TSQLRestServerDB(7ffff7f99590) 6.09ms DELETE FROM AuthUser WHERE RowID=:(11):;
20210224 13241602 # srvr mORMotSQLite3.TSQLRestServerDB(7ffff7f99590) Admin 172.18.0.3 DELETE root/AuthUser Write=200 out=0 B in 6.16ms
20210224 13241602 # - 00.006.186
But at the browser's side, when inspecting the network debugging info, at the Timings tab there is a 10.06 sec waiting time for the DELETE request to get its HTTP_OK (200) response code. Is this an on purpose security measure by mORMot or not?
Thanks a lot ab you are very clear in functional terms, but my problem is how to make the system interpret the enumerated code returned by the interface method to respond with an HTTP_BADREQUEST, together with a json with ErrorCode, ErrorText properties. By just returning the second enumerated value the system responds with an HTTP_OK and the web client interprets this as a normal execution.
My (seemly unorthodox) solution is the following:
1. Inherit the service class from an TInterfacedObjectWithCustomCreate and add a method of TNotifyErrorURI type, where I catch the EServiceExcepton and call Ctx.Error accordingly:
function TBareService.NotifyErrorURI(Ctxt: TSQLRestServerURIContext;
E: Exception): boolean;
begin
if E is EServiceException then
begin
Ctxt.Error(E.Message);
Result := false
end
else Result := true
end;
2. In the overridden constructor I linked the sever's OnErrorURI event to the aforementioned method:
inherited Create;
aServer.OnErrorURI := @NotifyErrorURI;
3. Raise inside the service method an EServiceException according to my error checking logic with a descriptive message to the client.
I would clearly prefer the return of enumerated values but obviously I missed the way to do it, as I explained in my first paragraph.
Ps. This topic is similar to the following old forum topic: https://synopse.info/forum/viewtopic.php?id=4293
OK thanks, but should I use TCQRSQueryObject and ICQRSService whenever I want a proper error handling? If I have a simple TInterfacedObject descendant service, implementing an IInvokable descendant interface what should I use inside a service method to pass the error to the web client?
As far as I know, by my testings and by reading also the following reply of ab in a previous forum topic:
https://synopse.info/forum/viewtopic.php?pid=9445#p9445
error messages in interface bases services are returning to clients using exceptions. I've noticed however that this is returned to a web client not in the usual json format with ErrorCode, ErrorText properties. Instead the repsonseJSON is an object, with its textual representation given by another property, responseText as follows:
responseText: "{\r\n\"errorCode\":500,\r\n\"error\":\r\n{\"EServiceException\":{\r\n\t\"ClassName\":\"EServiceException\",\r\n\t\"Address\":\" $0000000000690839 CHANGEPASSWORD, line 36 of bareservice.pas\",\r\n\t\"Message\": \"My error message\"\r\n}}\r\n}"
Is there any way to handle errors in interface based service implementations, so to be sent back to web browsers as ErrorCode,ErrorText jsons with less severe error codes than 500?
Please let me know if the issue is gone or still exist on the latest mORMot revision when you add this line to the end of your source code.
Eugene with your modification the issue is gone, thanks a lot! Thanks also macfly for your kind suggestion.
I've downloaded release 2.4 (thanks Eugene ) and built and run the Demo in Linux x64 with FPC 3.2.0 / Lazarus 2.0.9. When the application is shutting down (after pressing the Enter key), it raises an access violation exception at line 982 of unit mORMotHttpServer, when it attempts to execute
fDBServers[0].Server.EndCurrentThread(Sender);
This happens also in Windows 10 64 bit with FPC/Lazarus but not with Delphi 10.3.
It might have something to do with the usage of IAutoFree, because using a classic try/finally approach doesn't raise any exception.
Start calls only the inherited method...
I missed one more call after the inherited Start method. This call in its simplest form (without any cache) is:
fMainRunner := TMVCRunOnRestServer.Create(Self);
I tried to test a bare, hello world MVC project in FPC/linux. I created an interface, IBareApplication descendant of IMVCApplication with no extra methods and a class, TBareApplication, descendant of TMVCApplication which implements the IBareApplication. This class has only two methods Start and Default. Start calls only the inherited method and Default is empty. Then I created the Views folder with Default and Error html files, the former contains the hello world message and the latter is a modified copy from the Sample 30.
The main file creates an empty model, an in memory Rest Server and the above application:
aModel := TSQLModel.Create([],'test');
try
aServer := TSQLRestServerFullMemory.Create(aModel);
try
aApplication := TBareApplication.Create;
try
aApplication.Start(aServer);
aHTTPServer := TSQLHttpServer.Create('8092',aServer);
try
aHTTPServer.RootRedirectToURI('test/default');
aServer.RootRedirectGet := 'test/default';
writeln('Running, press ENTER to exit');
readln;
finally
...
When I try to access localhost:8092/ it redirects to http://localhost:8092/test/default as expected, with a Json response contains an Error 400/Bad request message. What am I missing here?
I'm wandering if it would be possible to adapt the cross platform units (SynCrossPlatformSpecific, SynCrossPlatformREST, SynCrossPlatformCrypto) to be compatible also to pas2js. Ab do you have any hint / suggestion on what directive section to use as a basis, maybe the ISDWS? HASINLINE has to be included also?
I found a solution by passing window.location.hostname. Sorry for my rather silly question.
I modified slightly the Sample 27 - SMS cross platform client to use it in a reverse proxy nginx setup. It works only when the server address parameter of the GetClient method contains a specific address (either 127.0.0.1 or my local ip). Is there any way to pass a generic value to use the host ip address, whatever it is? I tried both the empty string and '/' without success.
By the way in the Smart Mobile Studio project, the assignment of Client to nil and the if check at the first line of the TForm1.BtnConnectClick event handler, doesn't seem to work:
if Client=nil then
...
else begin
...
Client.Free;
Client := nil;
end;
A second press on the 'Server Connect' button produces nothing because the Client object hasn't become nil. A raw and clumsy solution like the following, does the work properly:
if BtnConnect.Caption='Server Connect' then
...
BtnConnect.Caption := 'Disconnect';
...
else begin
...
BtnConnect.Caption := 'Server Connect';
...
end;