JDK-8158196 : WebView Form Post fails if connection is closed before keepAlive-Timeout
  • Type: Bug
  • Component: javafx
  • Sub-Component: web
  • Affected Version: 8,9
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_7
  • CPU: x86_64
  • Submitted: 2016-05-27
  • Updated: 2020-01-31
  • Resolved: 2017-02-13
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8u152Fixed 9Fixed
Related Reports
Duplicate :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.8.0_65"
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) Client VM (build 25.65-b01, mixed mode, sharing)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 6.1.7601]

A DESCRIPTION OF THE PROBLEM :
Load a page in a JavaFX WebView, which adds "Connection: keepAlive" and "Keep-Alive: timeout=..." headers.

If a POST request is done before the timeout is reached, but the connection is already closed by the server, the connection close is not recognized and the POST request fails.

If a debugger is attached to the WebEngine via webEngine.impl_getDebugger() with "Network.enable" this can be observed as 
"{"response":{"method":"Network.loadingFailed","params":{"errorText":"Software caused connection abort: recv failed","requestId":"0.502","timestamp":1.4641818912680063E9}},"timestamp":"15:11:31.278"}"

In the attaches sample the Keep-Alive-Timeout is set to 60 seconds, but Tomcat closes the connection after 30 seconds. So the POST fails if it is done between 30 and 60 seconds after initial page load.
Note that this does not need an configuration error on server side as used to reproduce, this also happens if the connection is closed through other means.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
The samples requieres an embedded Tomcat, therefore the samples contains multiple files with a gradle build to load the dependencies.

1. Start the main class "TimeoutSample": A JavaFX WebView should open with a simple form.
2. Wait till "click now" is ouput to the console and then click the submit button on the form.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
After the click the form should be submitted and the new page should display "post ok!"
ACTUAL -
Nothing happens.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
src/main/TimeoutSample.java
###
package main;
import java.io.File;
import java.util.Timer;

import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import servlet.TimeoutServlet;

public class TimeoutSample extends Application {

  public final static int TOMCAT_KEEPALIVE = 30000;
  
  public final static int HEADER_KEEPALIVE = 60000;
  
  @Override
  public void init() throws Exception {
    super.init();

    Tomcat tomcat = new Tomcat();
    tomcat.setPort( 8080 );
    tomcat.getConnector().setAttribute( "keepAliveTimeout", TOMCAT_KEEPALIVE );
    
    File base = new File(System.getProperty("java.io.tmpdir"));
    Context rootCtx = tomcat.addContext("/", base.getAbsolutePath());
    Tomcat.addServlet(rootCtx, "TimeoutServlet", new TimeoutServlet());
    rootCtx.addServletMapping("/test", "TimeoutServlet");
 
    tomcat.start();
  }

  @Override
  public void start( Stage primaryStage ) throws Exception {
    WebView webView = new WebView();
    StackPane root = new StackPane();
    root.getChildren().add( webView );
    Scene scene = new Scene( root, 300, 250 );

    primaryStage.setTitle( "TimeoutSample" );
    primaryStage.setScene( scene );
    primaryStage.show();

    webView.getEngine().load( "http://localhost:8080/test" );
    new Thread(()->{
      try {
        Thread.sleep( 30000 );
      } catch (Exception e) {
        e.printStackTrace();
      }
      System.out.println( "click now" );
    }).start();
  }

  public static void main( String[] args ) {
    launch( args );
  }
}
###

src/servlet/TimeoutServlet .java
###
package servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import main.TimeoutSample;

@WebServlet(name = "TimeoutServlet", urlPatterns = { "/test" })
public class TimeoutServlet extends HttpServlet {
  private static final long serialVersionUID = 425610949406102966L;

  @Override
  protected void doGet( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException {
    ServletOutputStream out = resp.getOutputStream();
    resp.addHeader("Connection", "Keep-Alive");
    resp.addHeader("Keep-Alive", "timeout=" + TimeoutSample.HEADER_KEEPALIVE);

    String form = "<html>" + "<head><title>Form</title></head>" + "<body>" + "<form method=\"post\">"
        + "<input type=\"text\" id=\"inputField\">" + "<button type=\"submit\">submit</button>" + "</form>" + "</body>"
        + "</html>";

    out.write( form.getBytes() );
    out.flush();
    out.close();
  }

  @Override
  protected void doPost( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException {
    ServletOutputStream out = resp.getOutputStream();
    out.write( "post ok!".getBytes() );
    out.flush();
    out.close();
  }
}

###

build.gradle
###
plugins {
  id 'java'
  id 'eclipse'
  id 'application'

}
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
  compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '8.5.2'
  compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-logging-juli', version: '8.5.2'
  
}

configurations.all {
  transitive = true
}

sourceSets {
  main {
    java {
      srcDirs = ['src']
    }
    resources {
      srcDirs = ['src']
    }
    resources.srcDirs = ['src']
  }
}
###

---------- END SOURCE ----------


Comments
Changeset: 0bb5f90e7796 Author: ghb Date: 2017-02-13 11:00 +0530 URL: http://hg.openjdk.java.net/openjfx/9-dev/rt/rev/0bb5f90e7796
13-02-2017

+1
10-02-2017

+1
10-02-2017

Updated webrev : http://cr.openjdk.java.net/~ghb/8158196/webrev.01/index.html if ("Connection reset".equals(ex.getMessage()) && connectionResetRetry) instead of if (ex.getMessage().equals("Connection reset") && connectionResetRetry) 2: Retry should be done only for "Connection reset" and only once. Exception message is not locale (ref http://hg.openjdk.java.net/jdk9/client/jdk/file/ba316e40c19b/src/java.base/share/classes/java/net/SocketInputStream.java#l210 ) Providing fix only based on exception type might lead to connection retry for SocketException("Socket closed"); which might not be a valid (retry once again from client end) scenario.
08-02-2017

1. Exception.getMessage is documented to allow for a possible null return value, so to bullet-proof the code you should either check for null or else switch the test as follows: "Connection reset".equals(ex.getMessage()) 2. I'm not sure whether an exact match is better or not, but startsWith() would be better than contains() if you go away from an exact match. Is there a way to determine whether you want to retry without looking at the message string at all? That seems fragile. We don't usually expect exception strings to be translated for different locales, but what if it were?
02-02-2017

I Would consider "Connection reset by peer" exception with valid use case / steps to reproduce.
25-01-2017

[~ghb] Regarding "Query 1: call stack of exception with message. I didn't see "Connection reset by peer" for this bug. " I mentioned in a broader sense of checking even server side exception for SocketException...
24-01-2017

Thank you [~mbilla] for your quick review. Query 1: call stack of exception with message. I didn't see "Connection reset by peer" for this bug. Changing to "equals()" to "contain()" would lead to unknown bug. java.net.SocketException: Connection reset at java.net.SocketInputStream.read(SocketInputStream.java:209) at java.net.SocketInputStream.read(SocketInputStream.java:141) at java.io.BufferedInputStream.fill(BufferedInputStream.java:246) at java.io.BufferedInputStream.read1(BufferedInputStream.java:286) at java.io.BufferedInputStream.read(BufferedInputStream.java:345) at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:704) at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:647) at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1569) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474) at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480) at com.sun.webkit.network.URLLoader.receiveResponse(URLLoader.java:434) at com.sun.webkit.network.URLLoader.doRun(URLLoader.java:166) at com.sun.webkit.network.URLLoader.lambda$run$0(URLLoader.java:131) at java.security.AccessController.doPrivileged(Native Method) at com.sun.webkit.network.URLLoader.run(URLLoader.java:130) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Wo Query 2 : This defect is all about form submit (i.e http post) and till now i have never seen form submit will happens to be a streaming case. In General and to make little clear on the Root cause and fix i have done. a. Socket timeout : Server side config which indents to keep the socket open at the server side. keepAliveTimeout which can be seen in the description. b. Keep-Alive:timeout=x milli second and Connection:Keep-Alive (Http header) for establishing persistent connection from client to server Vice versa w.r.t to the description keepAliveTimeout is set to 30 second Keep-Alive:timeout=60 second. (ref : http://docs.oracle.com/javase/8/docs/technotes/guides/net/http-keepalive.html) Server will keep the TCP connection alive for max 30 second (idle) and it resets the active port by settng flags [RST]. When client (our java.net.HttpURLConnection) tries to send form data in the same port after 30 second (which is not active / disconnected by the server) leads to "java.net.SocketException: Connection reset" exception and this wasn't captured earlier and form data was not submitted to server. Where as considering the same use case on Chrome / Firefox they worked by sending Form data in new TCP socket.
24-01-2017

Changes looks fine for this particular use case: Two Queries: Query 1: I think you should check for "contains()" instead of "equals()" for ex.getMessage().equals("Connection reset") Since for server side exception , the message will be "Connection reset by peer" . Query 2: (About Streaming case) SocketException might occur due to different reasons (http://stackoverflow.com/questions/62929/java-net-socketexception-connection-reset) . So in below cases, do we need to re-try with streaming or without streaming? 1. The other end has deliberately reset the connection, in a way which I will not document here. It is rare, and generally incorrect, for application software to do this, but it is not unknown for commercial software. 2. More commonly, it is caused by writing to a connection that the other end has already closed normally. In other words an application protocol error. 3. It can also be caused by closing a socket when there is unread data in the socket receive buffer.
24-01-2017

webrev : http://cr.openjdk.java.net/~ghb/8158196/webrev.00/ Root Cause : "java.net.SocketException: Connection reset" occurs while reading HttpURLConnection.getResponseCode(). Test application has Tomcat : keepAliveTimeout = 30 sec Http : Keep-Alive = 60 sec Server will terminate TCP socket after 30 sec, Client will try to use the same socket (which is terminated at server end) for sending the form data (POST) which leads to "Connection reset" exception. Solution : Create new connection (Only once) if there is a "SocketException". Tested on windows 7, ubuntu 16.04 and OS X.
24-01-2017

Raising to P3, since the issue seems quite serious.
30-08-2016

Behaves same on 1.8.0-b132, 8u66 and 9-dev.
14-06-2016