title: Java HTTP server sending chunked response
body: <p>I am working on a Java application which has a built in HTTP server, at the moment the server is implemented using ServerSocketChannel, it listens on port 1694 for requests:</p>
  
  <pre><code>        msvrCh = ServerSocketChannel.open();
          msvrCh.socket().bind(new InetSocketAddress(mintPort));
          msvrCh.configureBlocking(false);
  </code></pre>
  
  <p>A thread is installed to manage requests and responses:</p>
  
  <pre><code>        Thread thrd = new Thread(msgReceiver);
          thrd.setUncaughtExceptionHandler(exceptionHandler);
          thrd.start();
  </code></pre>
  
  <p>The thread is quite simple:</p>
  
  <pre><code>        Runnable msgReceiver = new Runnable() {
              @Override
              public void run() {
                  try{
                      while( !Thread.interrupted() ) {
      //Sleep a short period between checks for new requests                          
                          try{
                              Thread.sleep(DELAY_BETWEEN_ACCEPTS);
                          } catch(Exception ex) {
                              ex.printStackTrace();
                          }                           
                          SocketChannel cliCh = msvrCh.accept();
  
                          if ( blnExit() == true ) {
                              break;
                          }                           
                          if ( cliCh == null ) {
                              continue;
                          }
                          processRequest(cliCh.socket());
                      }                       
                  } catch (IOException ex) {
                      ex.printStackTrace();
                  } finally {                     
                      logMsg(TERMINATING_THREAD + 
                              "for accepting cluster connections", true);
  
                      if ( msvrCh != null ) {
                          try {
                              msvrCh.close();
                          } catch (IOException ex) {
                              ex.printStackTrace();
                          }
                          msvrCh = null;
                      }
                  }               
              }
          };
  </code></pre>
  
  <p>The main bulk of the code for dealing with the response is in the function processRequest:</p>
  
  <pre><code>private void processRequest(Socket sck) {
      try {
      //AJAX Parameters
          final String AJAX_ID            = "ajmid";
      //The 'Handler Key' used to decode response         
          final String HANDLER_KEY        = "hkey";
      //Message payload           
          final String PAYLOAD            = "payload";
      //Post input buffer size            
          final int REQUEST_BUFFER_SIZE   = 4096;
      //Double carriage return marks the end of the headers           
          final String CRLF               = "\r\n";
  
          BufferedReader in = new BufferedReader(new InputStreamReader(sck.getInputStream()));
          String strAMID = null, strHKey = null, strRequest;
          char[] chrBuffer = new char[REQUEST_BUFFER_SIZE];
          StringBuffer sbRequest = new StringBuffer();
          eMsgTypes eType = eMsgTypes.UNKNOWN;
          clsHTTPparameters objParams = null;
          int intPos, intCount;               
      //Extract the entire request, including headers         
          if ( (intCount = in.read(chrBuffer)) == 0 ) {
              throw new Exception("Cannot read request!");
          }
          sbRequest.append(chrBuffer, 0, intCount);           
          strRequest = sbRequest.toString();
      //What method is being used by this request?
          if ( strRequest.startsWith(HTTP_GET) ) {
      //The request should end with a HTTP marker, remove this before trying to interpret the data
              if ( strRequest.indexOf(HTTP_MARKER) != -1 ) {
                  strRequest = strRequest.substring(0, strRequest.indexOf(HTTP_MARKER)).trim();
              }            
      //Look for a data marker
              if ( (intPos = strRequest.indexOf(HTTP_DATA_START)) &gt;= 0 ) {
      //Data is present in the query, skip to the start of the data
                  strRequest = strRequest.substring(intPos + 1);
              } else {
      //Remove the method indicator
                  strRequest = strRequest.substring(HTTP_GET.length());                   
              }
          } else if ( strRequest.startsWith(HTTP_POST) ) {
      //Discard the headers and jump to the data
              if ( (intPos = strRequest.lastIndexOf(CRLF)) &gt;= 0 ) {
                  strRequest = strRequest.substring(intPos + CRLF.length());  
              }
          }
          if ( strRequest.length() &gt; 1 ) {
      //Extract the parameters                    
              objParams = new clsHTTPparameters(strRequest);
          }            
          if ( strRequest.startsWith("/") == true ) {
      //Look for the document reference
              strRequest = strRequest.substring(1);               
              eType = eMsgTypes.SEND_DOC;             
          }
          if ( objParams != null ) {
      //Transfer the payload to the request
              String strPayload = objParams.getValue(PAYLOAD);
  
              if ( strPayload != null ) {
                  byte[] arybytPayload = Base64.decodeBase64(strPayload.getBytes()); 
                  strRequest = new String(arybytPayload);
                  strAMID = objParams.getValue(AJAX_ID);
                  strHKey = objParams.getValue(HANDLER_KEY);
              }
          } 
          if ( eType == eMsgTypes.UNKNOWN 
            &amp;&amp; strRequest.startsWith("{") &amp;&amp; strRequest.endsWith("}") ) {
      //The payload is JSON, is there a type parameter?
              String strType = strGetJSONItem(strRequest, JSON_LBL_TYPE);
  
              if ( strType != null &amp;&amp; strType.length() &gt; 0 ) {
      //Decode the type                   
                  eType = eMsgTypes.valueOf(strType.toUpperCase().trim());
      //What system is the message from?
                  String strIP = strGetJSONItem(strRequest, JSON_LBL_IP)
                        ,strMAC = strGetJSONItem(strRequest, JSON_LBL_MAC);                   
                  if ( strIP != null &amp;&amp; strIP.length() &gt; 0
                   &amp;&amp; strMAC != null &amp;&amp; strMAC.length() &gt; 0 ) {
      //Is this system known in the cluster?
                      clsIPmon objSystem = objAddSysToCluster(strIP, strMAC);
  
                      if ( objSystem != null ) {
      //Update the date/time stamp of the remote system                           
                          objSystem.touch();                          
                      }
      //This is an internal cluster message, no response required
                      return;
                  }                   
              }
          }            
          String strContentType = null, strRespPayload = null;
          OutputStream out = sck.getOutputStream();
          byte[] arybytResponse = null;
          boolean blnShutdown = false;
          out.write("HTTP/1.0 200\n".getBytes());
  
          switch( eType ) {
          case SEND_DOC:
              if ( strRequest.length() &lt;= 1 ) {
                  strRequest = HTML_ROOT + DEFAULT_DOC;
              } else {
                  strRequest = HTML_ROOT + strRequest;
              }
              logMsg("HTTP Request for: " + strRequest, true);
  
              if ( strRequest.toLowerCase().endsWith(".css") == true ) {
                  strContentType = MIME_CSS;
              } else if ( strRequest.toLowerCase().endsWith(".gif") == true ) {
                  strContentType = MIME_GIF;
              } else if ( strRequest.toLowerCase().endsWith(".jpg") == true ) {
                  strContentType = MIME_JPG;
              } else if ( strRequest.toLowerCase().endsWith(".js") == true ) {
                  strContentType = MIME_JS;
              } else if ( strRequest.toLowerCase().endsWith(".png") == true ) {
                  strContentType = MIME_PNG;
              } else if ( strRequest.toLowerCase().endsWith(".html") == true 
                       || strRequest.toLowerCase().endsWith(".htm") == true ) {
                  strContentType = MIME_HTML;
              }
              File objFile = new File(strRequest);
  
              if ( objFile.exists() == true ) {
                  FileInputStream objFIS = new FileInputStream(objFile);
  
                  if ( objFIS != null ) {
                      arybytResponse = new byte[(int)objFile.length()];
  
                      if ( objFIS.read(arybytResponse) == 0 ) {
                          arybytResponse = null;
                      }
                      objFIS.close();
                  }
              }
              break;
          case CHANNEL_STS:
              strRespPayload = strChannelStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case CLUSTER_STS:
              strRespPayload = strClusterStatus();
              strContentType = MIME_JSON; 
              break;
          case MODULE_STS:
              strRespPayload = strModuleStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case NETWORK_INF:
              strRespPayload = strNetworkInfo(strRequest);
              strContentType = MIME_JSON;
              break;
          case NODE_STS:
              strRespPayload = strNodeStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case POLL_STS:
              strRespPayload = strPollStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case SYS_STS:
      //Issue system status               
              strRespPayload = strAppStatus();
              strContentType = MIME_JSON;
              break;          
          case SHUTDOWN:
      //Issue instruction to restart system
              strRespPayload = "Shutdown in progress!";
              strContentType = MIME_PLAIN;
      //Flag that shutdown has been requested             
              blnShutdown = true;
              break;
          default:
          }
          if ( strRespPayload != null ) {
      //Convert response string to byte array             
              arybytResponse = strRespPayload.getBytes();
      System.out.println("[ " + strRespPayload.length() + " ]: " + strRespPayload);           //HACK          
          }           
          if ( arybytResponse != null &amp;&amp; arybytResponse.length &gt; 0 ) {
              if ( strContentType == MIME_JSON ) {
                  String strResponse = "{";
  
                  if ( strAMID != null ) {
      //Include the request AJAX Message ID in the response
                      if ( strResponse.length() &gt; 1 ) {
                          strResponse += ",";
                      }   
                      strResponse += "\"" + AJAX_ID + "\":" + strAMID;
                  }
                  if ( strHKey != null ) {
                      if ( strResponse.length() &gt; 1 ) {
                          strResponse += ",";
                      }
                      strResponse += "\"" + HANDLER_KEY + "\":\"" + strHKey + "\"";
                  }
                  if ( strResponse.length() &gt; 1 ) {
                      strResponse += ",";
                  }
                  strResponse += "\"payload\":" + new String(arybytResponse) 
                               + "}";                 
                  arybytResponse = strResponse.getBytes();
              }
              String strHeaders = "";
  
              if ( strContentType != null ) {
                  strHeaders += "Content-type: " + strContentType + "\n";                 
              }
              strHeaders += "Content-length: " + arybytResponse.length + "\n" 
                          + "Access-Control-Allow-Origin: *\n"
                          + "Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT\n"
                          + "Access-Control-Allow-Credentials: true\n"
                          + "Keep-Alive: timeout=2, max=100\n"
                          + "Cache-Control: no-cache\n" 
                          + "Pragma: no-cache\n\n";
              out.write(strHeaders.getBytes());
              out.write(arybytResponse);
              out.flush();                
          }
          out.close();
          sck.close();
  
          if ( blnShutdown == true ) {
              String strSystem =  mobjLocalIP.strGetIP();
  
              if ( strSystem.compareTo(mobjLocalIP.strGetIP()) != 0 ) {
      //Specified system is not the local system, issue message to remote system.
                  broadcastMessage("{\"" + JSON_LBL_TYPE  + "\":\"" + 
                                                     eMsgTypes.SHUTDOWN + "\""
                                 + ",\"" + JSON_LBL_TIME  + "\":\"" + 
                                             clsTimeMan.lngTimeNow() + "\"}");                            
              } else {
      //Shutdown addressed to local system                    
                  if ( getOS().indexOf("linux") &gt;= 0 ) {
      //TO DO!!!                  
                  } else if ( getOS().indexOf("win") &gt;= 0 ) {
                      Runtime runtime = Runtime.getRuntime();
                      runtime.exec("shutdown /r /c \"Shutdown request\" /t 0 /f");
                      System.exit(EXITCODE_REQUESTED_SHUTDOWN);
                  }               
              }
          }
      } catch (Exception ex) {            
      } finally {
          if (sck != null) {
              try {
                  sck.close();
              } catch (IOException ex) {
                  ex.printStackTrace();
              }
          }
      }
  }
  </code></pre>
  
  <p>I would like to implemented a chunked response, at present chunked responses are not supported by the code above.  </p>
  
  <p>[Edit] I've tried to implement a chunked response by adding the method:</p>
  
  <pre><code>    /**
       * @param strData - The data to split into chunks
       * @return A string array containing the chunks
       */
   public static String[] arystrChunkData(String strData) {
      int intChunks = (strData.length() / CHUNK_THRESHOLD_BYTESIZE) + 1;
      String[] arystrChunks = new String[intChunks];
      int intLength = strData.length(), intPos = 0;
  
      for( int c=0; c&lt;arystrChunks.length; c++ ) {            
          if ( intPos &lt; intLength ) {
      //Extract a chunk from the data         
              int intEnd = Math.min(intLength - 1, intPos + CHUNK_THRESHOLD_BYTESIZE);
              arystrChunks[c] = strData.substring(intPos, intEnd);
          }
      //Advance data position to next chunk           
          intPos += CHUNK_THRESHOLD_BYTESIZE;
      }       
      return arystrChunks;
  }
  </code></pre>
  
  <p>The modified processRequest now looks like this:</p>
  
  <pre><code>        private void processRequest(Socket sck) {
      try {
          //AJAX Parameters
          final String AJAX_ID            = "ajmid";
          //The 'Handler Key' used to decode response         
          final String HANDLER_KEY        = "hkey";
          //Message payload           
          final String PAYLOAD            = "payload";
          //Post input buffer size            
          final int REQUEST_BUFFER_SIZE   = 4096;
          //Double carriage return marks the end of the headers           
          final String CRLF               = "\r\n";
  
          BufferedReader in = new BufferedReader(new InputStreamReader(sck.getInputStream()));
          String strAMID = null, strHKey = null, strRequest;
          char[] chrBuffer = new char[REQUEST_BUFFER_SIZE];
          StringBuffer sbRequest = new StringBuffer();
          eMsgTypes eType = eMsgTypes.UNKNOWN;
          clsHTTPparameters objParams = null;
          int intPos, intCount;               
          //Extract the entire request, including headers         
          if ( (intCount = in.read(chrBuffer)) == 0 ) {
              throw new Exception("Cannot read request!");
          }
          sbRequest.append(chrBuffer, 0, intCount);           
          strRequest = sbRequest.toString();
          //What method is being used by this request?
          if ( strRequest.startsWith(HTTP_GET) ) {
          //The request should end with a HTTP marker, remove this before trying to interpret the data
              if ( strRequest.indexOf(HTTP_MARKER) != -1 ) {
                  strRequest = strRequest.substring(0, strRequest.indexOf(HTTP_MARKER)).trim();
              }            
          //Look for a data marker
              if ( (intPos = strRequest.indexOf(HTTP_DATA_START)) &gt;= 0 ) {
          //Data is present in the query, skip to the start of the data
                  strRequest = strRequest.substring(intPos + 1);
              } else {
          //Remove the method indicator
                  strRequest = strRequest.substring(HTTP_GET.length());                   
              }
          } else if ( strRequest.startsWith(HTTP_POST) ) {
          //Discard the headers and jump to the data
              if ( (intPos = strRequest.lastIndexOf(CRLF)) &gt;= 0 ) {
                  strRequest = strRequest.substring(intPos + CRLF.length());  
              }
          }
          if ( strRequest.length() &gt; 1 ) {
          //Extract the parameters                    
              objParams = new clsHTTPparameters(strRequest);
          }            
          if ( strRequest.startsWith("/") == true ) {
          //Look for the document reference
              strRequest = strRequest.substring(1);               
              eType = eMsgTypes.SEND_DOC;             
          }
          if ( objParams != null ) {
          //Transfer the payload to the request
              String strPayload = objParams.getValue(PAYLOAD);
  
              if ( strPayload != null ) {
                  byte[] arybytPayload = Base64.decodeBase64(strPayload.getBytes()); 
                  strRequest = new String(arybytPayload);
                  strAMID = objParams.getValue(AJAX_ID);
                  strHKey = objParams.getValue(HANDLER_KEY);
              }
          } 
          if ( eType == eMsgTypes.UNKNOWN 
            &amp;&amp; strRequest.startsWith("{") &amp;&amp; strRequest.endsWith("}") ) {
          //The payload is JSON, is there a type parameter?
              String strType = strGetJSONItem(strRequest, JSON_LBL_TYPE);
  
                if ( strType != null &amp;&amp; strType.length() &gt; 0 ) {
          //Decode the type                   
                  eType = eMsgTypes.valueOf(strType.toUpperCase().trim());
          //What system is the message from?
                  String strIP = strGetJSONItem(strRequest, JSON_LBL_IP)
                        ,strMAC = strGetJSONItem(strRequest, JSON_LBL_MAC);                   
                  if ( strIP != null &amp;&amp; strIP.length() &gt; 0
                   &amp;&amp; strMAC != null &amp;&amp; strMAC.length() &gt; 0 ) {
          //Is this system known in the cluster?
                      clsIPmon objSystem = objAddSysToCluster(strIP, strMAC);
  
                      if ( objSystem != null ) {
          //Update the date/time stamp of the remote system                           
                          objSystem.touch();                          
                      }
          //This is an internal cluster message, no response required
                      return;
                  }                   
              }
          }            
          String strContentType = null, strRespPayload = null;            
          OutputStream out = sck.getOutputStream();
          byte[] arybytResponse = null;
          boolean blnShutdown = false;
          //Start the writing the headers
          String strHeaders = "HTTP/1.0 200\n"
                            + "Date: " + (new Date()).toString() + "\n"
                            + "Access-Control-Allow-Origin: *\n"
                            + "Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT\n"
                            + "Access-Control-Allow-Credentials: true\n"
                            + "Keep-Alive: timeout=2, max=100\n"
                            + "Cache-Control: no-cache\n" 
                            + "Pragma: no-cache\n";            
          out.write(strHeaders.getBytes());
          strHeaders = "";
  
          switch( eType ) {
          case SEND_DOC:
              if ( strRequest.length() &lt;= 1 ) {
                  strRequest = HTML_ROOT + DEFAULT_DOC;
              } else {
                  strRequest = HTML_ROOT + strRequest;
              }
              logMsg("HTTP Request for: " + strRequest, true);
  
              if ( strRequest.toLowerCase().endsWith(".css") == true ) {
                  strContentType = MIME_CSS;
              } else if ( strRequest.toLowerCase().endsWith(".gif") == true ) {
                  strContentType = MIME_GIF;
              } else if ( strRequest.toLowerCase().endsWith(".jpg") == true ) {
                  strContentType = MIME_JPG;
              } else if ( strRequest.toLowerCase().endsWith(".js") == true ) {
                  strContentType = MIME_JS;
              } else if ( strRequest.toLowerCase().endsWith(".png") == true ) {
                  strContentType = MIME_PNG;
              } else if ( strRequest.toLowerCase().endsWith(".html") == true 
                       || strRequest.toLowerCase().endsWith(".htm") == true ) {
                  strContentType = MIME_HTML;
              }
              File objFile = new File(strRequest);
  
              if ( objFile.exists() == true ) {
                  FileInputStream objFIS = new FileInputStream(objFile);
  
                  if ( objFIS != null ) {
                      arybytResponse = new byte[(int)objFile.length()];
  
                      if ( objFIS.read(arybytResponse) == 0 ) {
                          arybytResponse = null;
                      }
                      objFIS.close();
                  }
              }
              break;
          case CHANNEL_STS:
              strRespPayload = strChannelStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case CLUSTER_STS:
              strRespPayload = strClusterStatus();
              strContentType = MIME_JSON; 
              break;
          case MODULE_STS:
              strRespPayload = strModuleStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case NETWORK_INF:
              strRespPayload = strNetworkInfo(strRequest);
              strContentType = MIME_JSON;
              break;
          case NODE_STS:
              strRespPayload = strNodeStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case POLL_STS:
              strRespPayload = strPollStatus(strRequest);
              strContentType = MIME_JSON;
              break;
          case SYS_STS:
          //Issue system status               
              strRespPayload = strAppStatus();
              strContentType = MIME_JSON;
              break;          
          case SHUTDOWN:
          //Issue instruction to restart system
              strRespPayload = "Shutdown in progress!";
              strContentType = MIME_PLAIN;
          //Flag that shutdown has been requested             
              blnShutdown = true;
              break;
          default:
          }
          if ( strRespPayload != null ) {
          //Convert response string to byte array             
              arybytResponse = strRespPayload.getBytes();
          }           
          if ( arybytResponse != null &amp;&amp; arybytResponse.length &gt; 0 ) {
              boolean blnChunked = false;
  
              if ( strContentType != null ) {
                  strHeaders += "Content-type: " + strContentType + "\n";                 
              }               
              if ( strContentType == MIME_JSON ) {
                  String strResponse = "{";
  
                  if ( strAMID != null ) {
          //Include the request AJAX Message ID in the response
                      if ( strResponse.length() &gt; 1 ) {
                          strResponse += ",";
                      }   
                      strResponse += "\"" + AJAX_ID + "\":" + strAMID;
                  }
                  if ( strHKey != null ) {
                      if ( strResponse.length() &gt; 1 ) {
                          strResponse += ",";
                      }
                      strResponse += "\"" + HANDLER_KEY + "\":\"" + strHKey + "\"";
                  }
                  if ( strResponse.length() &gt; 1 ) {
                      strResponse += ",";
                  }
                  strResponse += "\"payload\":" + new String(arybytResponse) 
                               + "}";
          //How big is the response?
      if ( strResponse.length() &gt; CHUNK_THRESHOLD_BYTESIZE ) {
                      blnChunked = true;
                      strHeaders += "Transfer-Encoding: chunked\n\n";
                      out.write(strHeaders.getBytes());
          //Slice up the string into chunks
                              String[] arystrChunks = arystrChunkData(strResponse);
  
                      for( int c=0; c&lt;arystrChunks.length; c++ ) {
                          String strChunk = arystrChunks[c];
  
                          if ( strChunk != null ) {
                              String strLength = Integer.toHexString(strChunk.length()) + "\r\n";
                              strChunk += "\r\n";
                              out.write(strLength.getBytes());
                              out.write(strChunk.getBytes());
                          }                           
                      }
          //Last chunk is always 0 bytes                      
                      out.write("0\r\n\r\n".getBytes());
                  } else {
                      arybytResponse = strResponse.getBytes();
                  }
              }
              if ( blnChunked == false ) {    
                  strHeaders += "Content-length: " + arybytResponse.length + "\n\n";                          
                  out.write(strHeaders.getBytes());
                  out.write(arybytResponse);
              }
              out.flush();                
          }
          out.close();
          sck.close();
  
          if ( blnShutdown == true ) {
              String strSystem =  mobjLocalIP.strGetIP();
  
              if ( strSystem.compareTo(mobjLocalIP.strGetIP()) != 0 ) {
          //Specified system is not the local system, issue message to remote system.
                  broadcastMessage("{\"" + JSON_LBL_TYPE  + "\":\"" + 
                                                     eMsgTypes.SHUTDOWN + "\""
                                 + ",\"" + JSON_LBL_TIME  + "\":\"" + 
                                             clsTimeMan.lngTimeNow() + "\"}");                            
              } else {
      //Shutdown addressed to local system                    
                  if ( getOS().indexOf("linux") &gt;= 0 ) {
          //TO DO!!!                  
                  } else if ( getOS().indexOf("win") &gt;= 0 ) {
                      Runtime runtime = Runtime.getRuntime();
                      runtime.exec("shutdown /r /c \"Shutdown request\" /t 0 /f");
                      System.exit(EXITCODE_REQUESTED_SHUTDOWN);
                  }               
              }
          }
      } catch (Exception ex) {            
      } finally {
          if (sck != null) {
              try {
                  sck.close();
              } catch (IOException ex) {
                  ex.printStackTrace();
              }
          }
      }
  }
  </code></pre>
  
  <p>I've read several specifications for Chunked responses and as far as I can tell I am sending data in the correct format, however I don't receive anything in the browser.</p>
  
  <p>I may have wrongly assume that the browser would correctly piece together the chunks into one, but I could be wrong.  The client side handler looks like this:</p>
  
  <pre><code>      this.responseHandler = function() {
  try {    
    if ( mobjHTTP == null
    || !(mobjHTTP.readyState == 4 &amp;&amp; mobjHTTP.status == 200)
    || !(mstrResponseText = mobjHTTP.responseText)
    || mstrResponseText.length == 0 ) {
      //Not ready or no response to decode      
      return;
    }
      //Do something with the response
      } catch( ex ) {
    T.error("responseHandler:", ex);
  }
  </code></pre>
  
  <p>};</p>
  
  <p>This handler is set-up elsewhere in the object:</p>
  
  <pre><code>    mobjHTTP.onreadystatechange = this.responseHandler;
  </code></pre>