Skip to content

Commit fc6d254

Browse files
thimioschadlwilson
authored andcommitted
[fix] avoid infinite loop in Response#isClientAbortException, fixes #449.
(cherry picked from commit aec35a5)
1 parent bed1777 commit fc6d254

3 files changed

Lines changed: 58 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.2.8 (UNRELEASED)
2+
3+
- Fix possible infinite loop in Response#isClientAbortException (#449, #450)
4+
15
## 1.2.7
26

37
- Ensure compatibility with JRuby 10.0 and 10.1 (#419, #423)

src/main/java/org/jruby/rack/ext/Response.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,15 +619,23 @@ private boolean handledAsClientAbort(final Exception ioe) {
619619
return false;
620620
}
621621

622+
/**
623+
* How many nested causes {@link #isClientAbortException(Exception)} inspects.
624+
* A cause chain is allowed to be cyclic, thus the traversal needs a bound.
625+
*/
626+
private static final int MAX_CAUSE_DEPTH = 20;
627+
622628
// ioe.inspect =~ /(ClientAbortException|EofException|broken pipe)/i
623629
protected boolean isClientAbortException(final Exception ioe) {
624630
String error = ioe.toString();
625631
if ( error.contains("ClientAbortException") ) return true;
626632
if ( error.contains("EofException") ) return true;
627-
while ( true ) {
633+
Throwable cause = ioe;
634+
for ( int depth = 0; depth < MAX_CAUSE_DEPTH; depth++ ) {
628635
if ( error.toLowerCase().contains("broken pipe") ) return true;
629-
if ( ioe.getCause() == null ) break;
630-
error = ioe.getCause().getMessage();
636+
cause = cause.getCause();
637+
if ( cause == null ) break;
638+
error = cause.getMessage();
631639
if ( error == null ) break;
632640
}
633641
return false;

src/spec/ruby/jruby/rack/response_spec.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,49 @@ def flush
519519
end
520520
end
521521

522+
it "raises exceptions with a nested cause that do not look like abort exceptions" do
523+
servlet_response = org.jruby.rack.mock.fail.FailingHttpServletResponse.new
524+
# e.g. Jetty failing a stalled write: IOException caused by a TimeoutException
525+
servlet_response.setFailure java.io.IOException.new(
526+
java.util.concurrent.TimeoutException.new('Idle timeout expired: 30000/30000 ms')
527+
)
528+
begin
529+
with_swallow_client_abort do
530+
response.write_body new_response_environment(servlet_response)
531+
end
532+
fail 'IO exception NOT raised!'
533+
rescue java.io.IOException => e
534+
expect(e.to_s).to match(/idle timeout expired/i)
535+
end
536+
end
537+
538+
it "swallows client abort exceptions nested deeper in the cause chain" do
539+
servlet_response = org.jruby.rack.mock.fail.FailingHttpServletResponse.new
540+
servlet_response.setFailure java.io.IOException.new('write failed',
541+
java.io.IOException.new('connection problem', java.io.IOException.new('Broken pipe'))
542+
)
543+
with_swallow_client_abort do
544+
response.write_body new_response_environment(servlet_response)
545+
end
546+
end
547+
548+
it "raises (and does not hang) on a cyclic cause chain" do
549+
failure = java.io.IOException.new 'write failed'
550+
cause = java.io.IOException.new 'nested failure'
551+
failure.initCause cause
552+
cause.initCause failure
553+
servlet_response = org.jruby.rack.mock.fail.FailingHttpServletResponse.new
554+
servlet_response.setFailure failure
555+
begin
556+
with_swallow_client_abort do
557+
response.write_body new_response_environment(servlet_response)
558+
end
559+
fail 'IO exception NOT raised!'
560+
rescue java.io.IOException => e
561+
expect(e.to_s).to match(/write failed/i)
562+
end
563+
end
564+
522565
private
523566

524567
def with_dechunk(dechunk = true)

0 commit comments

Comments
 (0)