5

I have an Android application communicating with a Delphi 2006 web service application using Indy 10 TIdHttpServer (coming with Delphi 2006). The Delphi application generates a big XML file and serves this. The XML generation may last more than 5 minutes.

If the duration of GenerateXml() is more than about 5 minutes (*), I detect an error 10053 in TIdHTTPResponseInfo.WriteContent if running in the Delphi IDE:

Socket Error # 10053 Software caused connection abort.

However, on the android side nothing is detected and the HttpGet-call lasts forever.

My questions are:

1.) Why do I get the error 10053 and how can I avoid it? It seems like android times out the connection, but http.socket.timeout is set to infinite.

and

2.) What can I do to detect such an error on the client side (other than setting timeout, which would have to be too big to be useful)? Can I do something in TIdHttpServer.OnException?

Here is my code. Android - download function, which is run inside an AsyncTask:

protected static HttpEntity downloadEntity(String url) throws IOException {
    HttpClient client = new DefaultHttpClient();  

    //Check because of Error 10053: but timeout is null -> infinite
    Log.d("TAG", "http.socket.timeout: " + client.getParams().getParameter("http.socket.timeout"));

    HttpGet get = new HttpGet(url);
    HttpResponse response;
    try {
        //in case of Error 10053 the following call seems to last forever (in PlainSocketImpl.read)
        response = client.execute(get);
    } catch (ClientProtocolException e) {
        //...
    }

    //...

    return response.getEntity();  
}   

Delphi implementation of TIdHttpServer.OnCommandGet:

procedure ServeXmlDoc(XmlDoc: IXMLDocument; ResponseInfo: TIdHTTPResponseInfo);
var
    TempStream: TMemoryStream;
begin
    ResponseInfo.ContentType := 'text/xml';
    TempStream := TMemoryStream.Create;
    XMLDoc.SaveToStream(TempStream);
    ResponseInfo.FreeContentStream := True; 
    ResponseInfo.ContentStream := TempStream;
end;

procedure TMyService.HTTPServerCommandGet(AContext: TIdContext; RequestInfo: TIdHTTPRequestInfo;
  ResponseInfo: TIdHTTPResponseInfo);
begin
    Coinitialize(nil); 
    try
        //...
        ServeXmlDoc(GenerateXml(), ResponseInfo);
    finally
        CoUninitialize;
    end;
end;

Edit: (*) I have done further testing and experienced the error even in cases where the whole process had a duration of under 2 minutes.

Alois Heimer
  • 1,772
  • 1
  • 18
  • 40
  • Side note: if I cut the WLAN connection, I _do_ get an error in Android: java.net.SocketException recvfrom failed: ETIMEDOUT (Connection timed out) – Alois Heimer Sep 03 '13 at 21:33

1 Answers1

4

Something between Android and your server, such as a firewall/router, is likely cutting the connection after it is idle for too long. You should try enabling TCP keep-alives to avoid that.

On the other hand, this is the kind of situation that HTTP 1.1's chunked transfer encoding was designed to handle (assuming you are using HTTP 1.1 to begin with). Instead of waiting 5 minutes for the entire XML to be generated in full before then sending it to the client, you should send the XML in pieces as they are being generated. Not only does that keep the connection active, but it also reduces the server's memory footprint since it doesn't have to store the entire XML in memory at one time.

TIdHTTPServer does not (yet) natively support sending chunked responses (but TIdHTTP does support receiving chunked responses), however it would not be very difficult to implement manually. Write a custom TStream derived class and overwrite its virtual Write() method (or use Indy's TIdEventStream class) to write data to the HTTP client using the format outlined in RFC 2616 Section 3.6.1. With that, you can have ServeXmlDoc() set the ResponseInfo.TransferEncoding property to 'chunked' and call the ResponseInfo.WriteHeader() method without setting either the ResponseInfo.ContentText or ResponseInfo.ContentStream properties, then pass your custom stream to IXMLDocument.SaveToStream() so it will finish writing the response data after the headers. For example:

type
  TMyChunkedStream = class(TStream)
  private
    fIO: TIdIOHandler;
  public
    constructor Create(AIO: TIdIOHandler);
    function Write(const Buffer; Count: Longint): Longint; override;
    procedure Finished;
    ...
  end;

constructor TMyChunkedStream.Create(AIO: TIdIOHandler);
begin
  inherited Create;
  fIO := AIO;
end;

function TMyChunkedStream.Write(const Buffer; Count: Longint): Longint; override;
begin
  if Count > 0 then
  begin
    fIO.WriteLn(IntToHex(Count, 1));
    fIO.Write(RawToBytes(Buffer, Count));
    fIO.WriteLn;
  end;
  Result := Count;
end;

procedure TMyChunkedStream.Finished;
begin
  fIO.WriteLn('0');
  fIO.WriteLn;
end;
procedure ServeXmlDoc(XmlDoc: IXMLDocument; ResponseInfo: TIdHTTPResponseInfo);
var
  TempStream: TMyChunkedStream;
begin
  ResponseInfo.ContentType := 'text/xml';
  ResponseInfo.TransferEncoding := 'chunked';
  ResponseInfo.WriteHeader;

  TempStream := TMyChunkedStream.Create(ResponseInfo.Connection.IOHandler);
  try
    XMLDoc.SaveToStream(TempStream);
    TempStream.Finished;
  finally
    TempStream.Free;
  end;
end;

If, on the other hand, the bulk of your waiting is inside of GenerateXml() and not in XmlDoc.SaveToStream(), then you need to rethink your server design, and figure out a way to speed up GenerateXml(), or just get rid of IXMLDocument and create the XML manually so you can send it using the ResponseInfo.Connection.IOHandler as you are creating the XML content.

Community
  • 1
  • 1
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thank you very much for the detailed answer. I tried to enable TCP keep-alives using [SetSockOpt](http://stackoverflow.com/a/5893446/2523663). That did not help. Do I have to do something on the client side too? I agree, that getting rid of IXMLDocument and using chunked transfer encoding would be preferable, but that would be a huge task. (Btw I found no setting in my Huawei router or my Windows firewall where I could specify the mentioned timeouts.) – Alois Heimer Sep 04 '13 at 10:15
  • I would suggest enabling the TCP keepalive on the client side, not the server side. And not all routers/firewalls have an explicit setting for setting the idle timeout. – Remy Lebeau Sep 04 '13 at 15:11
  • It looks like it is [difficult](http://stackoverflow.com/q/6565667/2306907) to set the TCP keepalive timeout on android. I have given up on this. I have splitted the generated XML file into several files which solved the problem for now. Strangely, I had to disable the keepalives in the TIdHTTPServer otherwise I got the 10053-error even for the splitted XML file (after about 1 minute). Of course that is only a workaround. – Alois Heimer Sep 04 '13 at 19:54
  • I am worried that it is possible that exceptions can occur, that will mean my HttpGet-call lasts forever. As asked in my second question: Is there really no way to detect this kind of exception on the client side other than by timeout? – Alois Heimer Sep 04 '13 at 19:54
  • Android is actually Linux-based under the hood, and Linux supports the `TCP_KEEPIDLE` and `TCP_KEEPINTVL` socket options via the `setsockopt()` function. As for error handling, if an exception occurs on the server side, and you let the server handle it, then it will close the connection, which Android should be able to detect like any other disconnect. – Remy Lebeau Sep 04 '13 at 20:31
  • I commented in [that other discussion](http://stackoverflow.com/q/6565667/2306907) a possible way you might gain access to `TCP_KEEPIDLE` and `TCP_KEEPINTVL` in Android. – Remy Lebeau Sep 04 '13 at 22:40
  • Thanks, but I do not think, that I can workout the android stuff :( I have done some further testing, but no success until now. – Alois Heimer Sep 05 '13 at 11:43
  • Regarding your previous comment: The error handling _is_ done solely by the server. I only set `OnCommandXXX` callbacks of a stock `TIdHTTPServer` component. The error occures in `TIdIOHandlerStack.WriteDirect`, which is called _after_ completion of the `OnCommand`-function. I am not able react to the exception, because `OnException` is never reached (even logging is not possible this way). And most times android does _not_ detect the disconnect. If this error occurs, most times `DefaultHttpClient().execute(HttpGet(url))` just runs forever. – Alois Heimer Sep 05 '13 at 11:44
  • When the error occurs, the connection has already been lost. This still sounds like something sitting between Android and your server is killing an idle connection and not telling the client about it. You need to redesign your server to not let the connection be idle for so long. – Remy Lebeau Sep 05 '13 at 14:20
  • OK, thank you very much for your patience and your advice. I hoped there would be a workaround, but it seems, that I will have to bite the bullet and redesign the server. What I am afraid of nevertheless is, that something is able to kill the connection in a way, that the android client is stuck. – Alois Heimer Sep 05 '13 at 14:38
  • When a firewall/router kills an idle connection, they may or may not send a RST packet to all parties right away. Sounds like in this situation, your firewall/router is not doing that. On the other hand, you really should be using a timeout on the client side anyway, no matter what. You never know when a server (Indy or otherwise) may die, deadlock, get DOS'ed, etc. The client should abort the request if it takes a long time to get a response back. – Remy Lebeau Sep 05 '13 at 19:10
  • Thanks, I will set a timeout and reimplement the XML generation on the server. Unfortunately it looks like the rewrite of the XML generation is even [tougher than I thought](http://stackoverflow.com/q/18654127/2523663). – Alois Heimer Sep 06 '13 at 15:57
  • I just wanted to add, that I tested several WLAN router and the problems seem to occur only for one router type, backing the theory that the router might be the culprit. – Alois Heimer Sep 10 '13 at 19:03
  • I now tried to implement TMyChunkedStream, but it does not work with Indy 10.1.5, which came with Delphi 2006. I tried to replace `ResponseInfo.TransferEncoding := 'chunked';` with `ResponseInfo.RawHeaders.Values['Transfer-Encoding'] := 'chunked';` but this does not seem to be enough. Any chance to get this working in 10.1.5? – Alois Heimer Sep 13 '13 at 22:25
  • What problem are you having with it exactly? If there is no `TransferEncoding` property (I don't remember if there was or not), then use the `CustomHeaders` property instead of the `RawHeaders` property. – Remy Lebeau Sep 13 '13 at 23:22
  • Thanks, with `CustomHeaders` it now works nearly perfect. But the character encoding is not correct, only ASCII chars seem to be transmitted correctly. – Alois Heimer Sep 14 '13 at 01:49
  • `XmlDoc.SaveToStream()` writes out encoded bytes, so set `ResponseInfo.CharSet := '...'` (or `Response.ContentType := 'text/xml; charset=...'`) to match the actual encoding of your XML (utf-8 by default). – Remy Lebeau Sep 14 '13 at 01:57
  • I'm having this exact same problem (error message) but I do not have a very large or time consuming response. The server responds rather quickly with a simple JSON object. The request does however initialize COM and an ADO connection. Keep-alive is enabled on both client and server side. This error occurs when there are too many (but really not that many) concurrent requests from the same client. For example, put a URL to this server in a browser, hold "F5" to continuously refresh, not even 2 seconds later I get this error. – Jerry Dodge Aug 07 '14 at 13:09
  • Neither TIdHTTP nor TIdHTTPServer support HTTP pipelining. Holding F5 in a browser is going to make a socket connection, start a request, and then abort the request to make a new request, over and over. The only way to abort an HTTP request is to close the socket. Doing that enough times and 10053 is likely to occur sometimes. – Remy Lebeau Aug 07 '14 at 14:58