delphierror-handlingwinsockdbexpress

How can I get the Winsock error code for a dbExpress connect error?


In case of a connection problem, the dbExpress driver throws a TDBXError but does not include a socket error code. The message is simply:

Unable to complete network request to host "exampledb.local". Failed to establish a connection

Is there a way to retrieve the underlying socket error when this type of exception occurs?

The stack trace is:

main thread ($5934):
0061aa59 +051 example.exe DBXCommon    447  +0 TDBXContext.Error
00817f14 +10c example.exe DBXDynalink  796 +21 TDBXMethodTable.RaiseError
00818553 +013 example.exe DBXDynalink  949  +1 TDBXDynalinkConnection.CheckResult
00818744 +050 example.exe DBXDynalink 1048  +4 TDBXDynalinkConnection.DerivedOpen
0061750f +007 example.exe DBXCommon    447  +0 TDBXConnection.Open
00612fed +0f5 example.exe DBXCommon    447  +0 TDBXConnectionFactory.GetConnection
00612ef1 +005 example.exe DBXCommon    447  +0 TDBXConnectionFactory.GetConnection
0062c08f +26f example.exe SqlExpr              TSQLConnection.DoConnect
005d9019 +039 example.exe DB                   TCustomConnection.SetConnected
005d8fd4 +004 example.exe DB                   TCustomConnection.Open
0062b98f +01b example.exe SqlExpr              TSQLConnection.CheckConnection
0062ebdf +01f example.exe SqlExpr              TCustomSQLDataSet.CheckConnection
0062efe4 +04c example.exe SqlExpr              TCustomSQLDataSet.OpenCursor
005e91d5 +055 example.exe DB                   TDataSet.SetActive
005e901c +004 example.exe DB                   TDataSet.Open

Solution

  • (Delphi XE2 and 'gds32.dll' 10.0.1.335 (ansi) is used in this answer)

    dbExpress is a high level framework that delegates provider specific operations to its drivers. All of these provider specific drivers expose the same basic common functionality, hence it is not possible to retrieve specific information which falls outside this common functionality.

    Regarding error reporting, the drivers export two functions. Be it the mysql or the mssql or any other driver, these functions are DBXBase_GetErrorMessageLength and DBXBase_GetErrorMessage. The drivers get most of what went wrong from client libraries and report that information to the framework through these functions. Beyond what have already reported, it is not possible to get any more specific detail about what went wrong about connecting to a database server or any other operation.

    So it is up to the client libraries to include winsock error information or not. In the case of the interbase driver, which the error included in the question is from, this information is already included (when there is one).

    Here is an example case, an attempt to connect to an existing server, and to a port that is listened but not by the database server.

      Connection := TSQLConnection.Create(nil);
      Connection.DriverName := 'Interbase';
      Connection.Params.Values['Database'] := 'existingserver/80:database';
      try
        Connection.Open;
      except
        on E: TDBxError do begin
          writeln(E.Message + sLineBreak);
    

    Here is the output:

    Unable to complete network request to host "existingserver:80".
    Failed to establish a connection.

    As you notice this is exactly the same error message in the question. Note that there is no winsock error in the text. That is because there is no winsock error, as long as the network protocol is concerned, the connection is actually successful.


    Here is an attempt to an existing server, but to a non-listened port.

      Connection := TSQLConnection.Create(nil);
      Connection.DriverName := 'Interbase';
      Connection.Params.Values['Database'] := 'existingserver/81:database';
      try
        Connection.Open;
      except
        on E: TDBxError do begin
          writeln(E.Message + sLineBreak);
    

    Here is the returned error:

    Unable to complete network request to host "existingserver:81".
    Failed to establish a connection.
    unknown Win32 error 10060

    This time the connection fails since there is no listener for the specific port. I have no idea about why the client library fails to resolve 10060, but this is your winsock error.


    Case for localhost, non-listened port:

    Unable to complete network request to host "localhost:3051".
    Failed to establish a connection.
    No connection could be made because the target machine actively refused it.

    Here we have a 10061.


    When an attempt to connect to a non-resolvable host is made, gds32.dll does not report any api error. I don't know how the library resolves a host or why it doesn't include error code, but the error message is verbose:

    Unable to complete network request to host "nonexistingserver".
    Failed to locate host machine.
    The specified name was not found in the hosts file or Domain Name Services.



    If we were to directly use the client library, we could get only the api error. Examine the following program where a connection attempt is made to an existing server and a closed port.

    program Project1;
    
    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      system.sysutils,
      data.dbxcommon,
      data.sqlexpr,
      data.dbxinterbase;
    
    function isc_attach_database(status_vector: PLongint; db_name_length: Smallint;
        db_name: PAnsiChar; db_handle: PPointer; parm_buffer_length: Smallint;
        parm_buffer: PAnsiChar): Longint; stdcall; external 'gds32.dll';
    
    function isc_interprete(buffer: PAnsiChar; var status_vector: Pointer): Longint;
        stdcall; external 'gds32.dll';
    
    var
      Connection: TSQLConnection;
    
      StatusVector: array[0..19] of Longint;
      Handle: PPointer;
      Error: array[0..255] of AnsiChar;
      IntrStatus: Pointer;
      s: string;
    begin
      try
        Connection := TSQLConnection.Create(nil);
        Connection.DriverName := 'Interbase';
        Connection.Params.Values['Database'] := 'server/3051:database'; // closed port
        try
          Connection.Open;
        except
          on E: TDBxError do begin
            writeln(E.Message + sLineBreak);
    
            if E.ErrorCode = TDBXErrorCodes.ConnectionFailed then begin
    
              Handle := nil;
              if isc_attach_database(@StatusVector, 0,
                          PAnsiChar(AnsiString(Connection.Params.Values['Database'])),
                          @Handle, 0, nil) <> 0 then begin
    
                IntrStatus := @StatusVector;
                s := '';
                while isc_interprete(Error, IntrStatus) <> 0 do begin
                  s := s + AnsiString(Error) + sLineBreak;
                  if PLongint(IntrStatus)^ = 17 then  // isc_arg_win32
                    s := s + ' --below is an api error--' + sLineBreak +
                         Format('%d: %s', [PLongint(Longint(IntrStatus) + 4)^,
                         SysErrorMessage(PLongint(Longint(IntrStatus) + 4)^)]) +
                         sLineBreak;
                end;
                Writeln(s);
              end;
            end else
              raise;
          end;
        end;
      except
        on E: Exception do
          Writeln(E.ClassName, ': ', E.Message);
      end;
      readln;
    end.
    

    Which outputs:

    Unable to complete network request to host "sever:3051".
    Failed to establish a connection.
    unknown Win32 error 10060

    Unable to complete network request to host "server:3051".
    Failed to establish a connection.
    --below is an api error--
    10060: A connection attempt failed because the connected party did not properly
    respond after a period of time, or established connection failed because connect
    ed host has failed to respond
    unknown Win32 error 10060

    The first paragraph is what dbx reports. The second paragraph is what we get from 'gds32.dll' including the injection of api error code and text, otherwise they are the same.

    The above is a crude demonstration, for proper typing use 'interbase.h'. And for details about picking a possible api error, see "Parsing the Status Vector", or in general "Handling Error Conditions".



    In any case, as can be seen, being able to get specific information entirely depends on the client library that dbx uses to connect to the database server.

    For the general case, to get winsock error information independently from the database server being used, what you can do is to attempt to connect a socket to the server before trying to open a database connection, and only if this is successful close your test connection and then proceed attaching to the database. You can use any library or bare api to do this.

    Here is a simple example:

    function TestConnect(server: string; port: Integer): Boolean;
    
      procedure WinsockError(step: string);
      begin
        raise Exception.Create(Format('"%s" fail. %d: %s:',
            [step, WSAGetLastError, SysErrorMessage(WSAGetLastError)]));  
      end;
    
    var
      Error: Integer;
      Data: TWSAData;
      Socket: TSocket;
      SockAddr: TSockAddrIn;
      Host: PHostEnt;
    begin
      Result := False;
      Error := WSAStartup(MakeWord(1, 1), Data);
      if Error = 0 then begin
        try
          Socket := winsock.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
          if Socket <> INVALID_SOCKET then begin
            try
              Host := gethostbyname(PAnsiChar(AnsiString(server)));
              if Host <> nil then begin
                SockAddr.sin_family := AF_INET;
                SockAddr.sin_addr.S_addr := Longint(PLongint(Host^.h_addr_list^)^);
                SockAddr.sin_port := htons(port);
                if connect(Socket, SockAddr,  SizeOf(SockAddr)) <> 0 then
                  WinsockError('connect')
                else
                  Result := True;
              end else
                WinsockError('gethostbyname');
            finally
              closesocket(Socket);
            end;  
          end else
            WinsockError('socket');
        finally  
          WSACleanup;
        end;  
      end else
        raise Exception.Create('winsock initialization fail');
    end;
    

    You can use something like this like:

    if TestConnect('server', 3050) then
      //