Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/main/java/org/jruby/rack/ext/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -615,15 +615,23 @@ private boolean handledAsClientAbort(final Exception ioe) {
return false;
}

/**
* How many nested causes {@link #isClientAbortException(Exception)} inspects.
* A cause chain is allowed to be cyclic, thus the traversal needs a bound.
*/
private static final int MAX_CAUSE_DEPTH = 20;

// ioe.inspect =~ /(ClientAbortException|EofException|broken pipe)/i
protected boolean isClientAbortException(final Exception ioe) {
String error = ioe.toString();
if ( error.contains("ClientAbortException") ) return true;
if ( error.contains("EofException") ) return true;
while ( true ) {
Throwable cause = ioe;
for ( int depth = 0; depth < MAX_CAUSE_DEPTH; depth++ ) {
if ( error.toLowerCase().contains("broken pipe") ) return true;
if ( ioe.getCause() == null ) break;
error = ioe.getCause().getMessage();
cause = cause.getCause();
if ( cause == null ) break;
error = cause.getMessage();
if ( error == null ) break;
}
return false;
Expand Down
43 changes: 43 additions & 0 deletions src/spec/ruby/jruby/rack/response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,49 @@ def flush
end
end

it "raises exceptions with a nested cause that do not look like abort exceptions" do
servlet_response = org.jruby.rack.mock.fail.FailingHttpServletResponse.new
# e.g. Jetty failing a stalled write: IOException caused by a TimeoutException
servlet_response.setFailure java.io.IOException.new(
java.util.concurrent.TimeoutException.new('Idle timeout expired: 30000/30000 ms')
)
begin
with_swallow_client_abort do
response.write_body new_response_environment(servlet_response)
end
fail 'IO exception NOT raised!'
rescue java.io.IOException => e
expect(e.to_s).to match(/idle timeout expired/i)
end
end

it "swallows client abort exceptions nested deeper in the cause chain" do
servlet_response = org.jruby.rack.mock.fail.FailingHttpServletResponse.new
servlet_response.setFailure java.io.IOException.new('write failed',
java.io.IOException.new('connection problem', java.io.IOException.new('Broken pipe'))
)
with_swallow_client_abort do
response.write_body new_response_environment(servlet_response)
end
end

it "raises (and does not hang) on a cyclic cause chain" do
failure = java.io.IOException.new 'write failed'
cause = java.io.IOException.new 'nested failure'
failure.initCause cause
cause.initCause failure
servlet_response = org.jruby.rack.mock.fail.FailingHttpServletResponse.new
servlet_response.setFailure failure
begin
with_swallow_client_abort do
response.write_body new_response_environment(servlet_response)
end
fail 'IO exception NOT raised!'
rescue java.io.IOException => e
expect(e.to_s).to match(/write failed/i)
end
end

private

def with_dechunk(dechunk = true)
Expand Down
Loading