0

I'm writing a client app that connects to a service using the chunked transfer encoding. The service occasionally disconnects and I was told it was because we're sending a zero chunk in the request, so Tomcat closes the connection.

I'm using the Java HttpUrlConnection class to make the connection and I have no idea why it would be sending a zero chunk and how to prevent it from doing that.

Here's the code.

URL m5url = new URL("https://hostedconnect.m5net.com/bobl/bobl?name=org.m5.apps.v1.cti.ClickToDial.subscribe");
StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"standalone=\"yes\"?>" 
                                   + "<Command>" 
                                   + "<Name>org.m5.apps.v1.cti.ClickToDial.subscribe</Name>"
                                   + "<Id>1</Id>" 
                                   + "<User>" + m5username + "</User>" 
                                   + "<Password>" + m5password + "</Password>" 
                                   + "<FormattedXml>true</FormattedXml>" 
                                   + "<ShallowResponse>FULL</ShallowResponse>" 
                                   + "</Command>");

conn = (HttpURLConnection) m5url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setReadTimeout(SESSION_TIMEOUT);
conn.setChunkedStreamingMode(0);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);

out = new DataOutputStream(conn.getOutputStream());
conn.connect();
out.writeBytes(sb.toString());
out.flush();

When I do inputstream.readline it's null, but sometimes it works and sometimes it doesn't.

enter image description here Ok, so I'm thoroughly confused. I abandoned using the HttpURLConnection and started using the Socket class and writing all the headers and data manually. Without sending the zero chunk it seems to work all the time. With the zero chunk it seemed to be working all the time except when I ran it in the debugger, where it got the same error as above. So I put a sleep(100) after it sends the headers and before it sends the data and ran it without the debugger and it consistently got the error. So I assume in the HttpURLConnection class there's a delay after sending the headers which is why it works sometimes and doesn't other times. I could just not send the zero chunk but I'd really like to know why that would cause the error. Any ideas? I'm thinking there's a bug in Tomcat.

Here's the code.

public class M5Connection
{
    public static final String urlBase = "/bobl/bobl";
    public static final String ENCODING = "ISO-8859-1";
    public static final String DELIMITER = "\r\n";
    protected URL url;
    private InputStream inputStream;
    protected OutputStream outputStream;
    protected Socket socket;
    protected BufferedReader reader;
    private boolean bProcessedHeaders;

    protected String resp = null;
    protected String errorMessage = null;


    /**
     * Start a new connection to the BOBL server.
     * @param server server name:port to connect to
     * @throws IOException
     */
    protected void initConnection(String server, int timeout) throws IOException
    {
        url = new URL(server + urlBase);
        int port = url.getPort();
        if (server.startsWith("https"))
        {
            if (port == -1) port = 443;
            else
                if (port == 80 || port == -1)port = 8080;
        }

        if (server.startsWith("https") == false)
        {
            socket = new Socket(url.getHost(), port);
        }
        else
        {           
            SocketFactory socketFactory = SSLSocketFactory.getDefault();
            socket = socketFactory.createSocket(url.getHost(), port);
        }

        socket.setSoTimeout(timeout);
        socket.setKeepAlive(true);
        socket.setSoLinger(false, 0);
        inputStream = socket.getInputStream();
        outputStream = socket.getOutputStream();
        reader = new BufferedReader(new InputStreamReader(inputStream));
    }

    public void initHttpsConnection(String server, int timeout) throws IOException
    {
        initConnection(server,timeout);
        sendHeaders();
        bProcessedHeaders = false;
    }

    private void sendHeaders() throws IOException {
        String path = url.getPath();
        StringBuffer outputBuffer = new StringBuffer();
        outputBuffer.append("POST " + path + " HTTP/1.1" + DELIMITER);
        outputBuffer.append("Host: " + url.getHost() + DELIMITER);
        outputBuffer.append("User-Agent: CometTest" + DELIMITER);
        outputBuffer.append("Connection: keep-alive" + DELIMITER);
        outputBuffer.append("Content-Type: text/plain" + DELIMITER);
        outputBuffer.append("Transfer-Encoding: chunked" + DELIMITER);
        outputBuffer.append(DELIMITER);
        byte[] outputBytes = outputBuffer.toString().getBytes(ENCODING);
        outputStream.write(outputBytes);
        outputStream.flush();
    }

    /** Send some data to the server, HTTP/1.1 chunked style. */
    public void send(String chunkData) throws IOException {
        byte[] chunkBytes = chunkData.getBytes(ENCODING);
        String hexChunkLength = Integer.toHexString(chunkBytes.length);
        StringBuffer outputBuffer = new StringBuffer();
        outputBuffer.append(hexChunkLength);
        outputBuffer.append(DELIMITER);
        outputBuffer.append(chunkData);
        outputBuffer.append(DELIMITER);
        byte[] outputBytes = outputBuffer.toString().getBytes(ENCODING);
        outputStream.write(outputBytes);
        outputStream.flush();

        outputBuffer = new StringBuffer();
        outputBuffer.append("0");
        outputBuffer.append(DELIMITER);
        outputBuffer.append(DELIMITER);
        outputBytes = outputBuffer.toString().getBytes(ENCODING);
        outputStream.write(outputBytes);
        outputStream.flush();
    }

    /**
     * Wait for a response from the server.
     * @return the string that the server returned.
     * @throws IOException
     */
    public String getRawResponse() throws IOException
    {
        String s;

        // just after we connect we expect to see the HTTP headers. Read and discard
        if (!bProcessedHeaders) {
            while (true){
                String line = reader.readLine();
                System.out.println("HEADER: " + line);

                if (line == null || line.equals("\r\n") || line.equals(""))
                    break;
            }
            bProcessedHeaders = true;
        }

        while (true)
        {       
            s = getChunk();     

            if (s == null)
                return null;

            if (s.equals("")) {
                continue;
            }

            // server will not emit XML if it is having real troubles
            if (s.charAt(0) != '<' || s.startsWith("<html>")) {
                System.out.println("Server says: " + s);
                continue;
            }
            return s;
        }
    }   

    /**
     * Expect chunked excoding back from the server. Read and return a chunk.
     * @return a string containing the HTTP chunk
     * @throws IOException
     */
    private String getChunk() throws IOException
    {
        StringBuffer buf = new StringBuffer();
        while (true)
        {
        // HTTP chunked mode, expect to see a line with the length in hex of the chunk that follows
            String s = reader.readLine();           

            if (s == null)
                throw new IOException();
            if (s.length() == 0)
                continue;

            int toread;
            try {
                toread = Integer.parseInt(s, 16);
            } catch (NumberFormatException e) {
                System.out.println("Number format error: " + s);
                return "";
            }

            if (toread == 0)
            {
                return null;
            }

            // read the chunk
            char[] data = new char[toread];
            int read = 0;
            while (read != toread)
            {
                read += reader.read(data, read, toread - read);
            }
            buf.append(data, 0, read);

            // for some reason tomcat only sends data in up to 8192 byte chunks
            if (toread != 8192)
                break;
        }
        return buf.toString();
    }   

    public void close()
    {
        try { socket.close(); } catch (IOException e) {}
    }

    public static void main(String[] args) throws Exception
    {
        M5Connection cnx = new M5Connection();
        cnx.initHttpsConnection("https://hostedconnect.m5net.com/bobl/bobl?name=org.m5.apps.v1.cti.ClickToDial.subscribe", 0);

        Thread.sleep(100);
        //
        // Create and send an XML command to listen for call state changes on our TN
        //
        String format = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
        "<Command>" +
        "    <Name>org.m5.apps.v1.cti.ClickToDial.subscribe</Name>" +
        "    <Id>1</Id>" +
        "    <User></User>" +
        "    <Password></Password>" +
        "    <FormattedXml>true</FormattedXml>" +
        "    <ShallowResponse>FULL</ShallowResponse>" +
        "</Command>";
        String command = format;
        System.out.println("SENDING " + command + "\n ------------ ");
        cnx.send(command);

        //
        // Now just wait for the responses
        //
        while (true)
        {
            String resp = cnx.getRawResponse();
            System.out.println(resp);
        }
    }
}
user1309036
  • 61
  • 1
  • 1
  • 4
  • Have you tried using another number in setChunkedStreamingMode()? If I understand your problem correctly, that would control the chunk size according to http://docs.oracle.com/javase/7/docs/api/index.html. – Luis Feb 27 '13 at 15:15
  • Try calling `connect` before you call `getOutputStream`. I've never seen it done in the order you're using and I could imagine `connect` trampling over the existing streams. – Alan Krueger Feb 27 '13 at 15:27
  • I tried changing the setchunkedstreamingmode and moving the connect before the getoutputstream and it's still getting the problem. I attached an image of the request and response. – user1309036 Feb 27 '13 at 15:50
  • probably a bug. you don't need "chunked" here, since the length is fixed and known. – irreputable Feb 27 '13 at 16:31
  • The response needs to be chunked since the length is unknown, it sends packets to us as events keep coming into their system, so doesn't the request need to set the transfer encoding to chunked if the response is chunked? This is my first time dealing with the chunked encoding so I'm not sure. I took out the setchunkedstreamingmode and it just hangs on getting the inputstream. When you say bug, do you mean a bug in Tomcat? – user1309036 Feb 27 '13 at 16:48
  • The length of the request body is known, which is `sb.length()`. You need to set "Content-Length" header. See example http://www.xyzws.com/Javafaq/how-to-use-httpurlconnection-post-data-to-web-server/139 – irreputable Feb 27 '13 at 16:55
  • 1
    And your "Content-Type" is definitely incorrect, though the server seems to ignore it. Ask the service what's the expected Content-Type. – irreputable Feb 27 '13 at 16:57
  • The content type is text/plain, but yes, they're ignoring it anyway. I tried not sending it chunked but got the error "Failed: Expected HTTP chunked encoding for long running commands" so they're expecting it to be chunked. – user1309036 Feb 27 '13 at 18:13
  • I don't see anything wrong with the "0" length chunked block you displayed, that's normal but you might want to try ending with an empty line http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 – Luis Feb 27 '13 at 20:04
  • How would I end it with an empty line? I'm not even sure why it's writing the zero chunk. – user1309036 Feb 27 '13 at 21:20
  • ...I had written "terminate the string you're passing to your StringBuffer() with \r\n\r\n" but now I don't think it'd make a difference because the image you included seems to show an empty line after the 0. Now, the zero chunk is normal, see the section 3.6.1 I pasted before. – Luis Feb 27 '13 at 22:42
  • I found out that getting the inputstream would end the ouputstream. That makes sense I guess. – user1309036 Feb 28 '13 at 21:54

1 Answers1

3

I was told it was because we're sending a zero chunk in the request

You were misinformed. The final chunk of zero is correct, indicating the end of the transfer. See RFC 2616 #3.6.1. There's nothing wrong with sending one, and Tomcat shouldn't (and almost certainly doesn't) react by closing the connection.

user207421
  • 305,947
  • 44
  • 307
  • 483