Skip to content

Commit e075f34

Browse files
committed
better client EOF (socket abort "Broken pipe") detection in JRuby::Rack::Response
... we're now "officially" handling Jetty and maybe a bunch of others due EofException
1 parent 1cb258a commit e075f34

File tree

7 files changed

+338
-32
lines changed

7 files changed

+338
-32
lines changed

src/main/ruby/jruby/rack/response.rb

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
module JRuby
1111
module Rack
1212
# Takes a Rack response to map it into the Servlet world.
13-
#
13+
#
1414
# Assumes servlet containers auto-handle chunking when the output stream
1515
# gets flushed. Thus de-chunks data if Rack chunked them, to disable this
1616
# behavior execute the following before delivering responses :
@@ -22,7 +22,12 @@ class Response
2222
include org.jruby.rack.RackResponse
2323
java_import 'java.nio.ByteBuffer'
2424
java_import 'java.nio.channels.Channels'
25-
25+
26+
@@swallow_client_abort = true
27+
# Whether we swallow client abort exceptions (EOF received on the socket).
28+
def self.swallow_client_abort?; @@swallow_client_abort; end
29+
def self.swallow_client_abort=(flag); @@swallow_client_abort = !! flag; end
30+
2631
@@dechunk = nil
2732
# Whether responses should de-chunk data (when chunked response detected).
2833
def self.dechunk?; @@dechunk; end
@@ -42,7 +47,7 @@ def self.dechunk=(flag); @@dechunk = !! flag; end
4247
def self.channel_chunk_size; @@channel_chunk_size; end
4348
def self.channel_chunk_size=(size); @@channel_chunk_size = size; end
4449
def channel_chunk_size; self.class.channel_chunk_size; end
45-
50+
4651
@@channel_buffer_size = 16 * 1024 # 16 kB
4752
# Returns a byte buffer size that will be allocated when copying between
4853
# channels. This usually won't happen at all (unless you return an exotic
@@ -52,7 +57,7 @@ def channel_chunk_size; self.class.channel_chunk_size; end
5257
def self.channel_buffer_size; @@channel_buffer_size; end
5358
def self.channel_buffer_size=(size); @@channel_buffer_size = size; end
5459
def channel_buffer_size; self.class.channel_buffer_size; end
55-
60+
5661
# Expects a Rack response: [status, headers, body].
5762
def initialize(array)
5863
@status, @headers, @body = *array
@@ -93,7 +98,7 @@ def respond(response)
9398
write_body(response)
9499
end
95100
end
96-
101+
97102
# Writes the response status.
98103
# @see #respond
99104
def write_status(response)
@@ -114,7 +119,7 @@ def write_headers(response)
114119
# setContentLength(int) ... addHeader must be used for large files (>2GB)
115120
response.setContentLength(length) if ! chunked? && length < 2_147_483_648
116121
else
117-
# servlet container auto handle chunking when response is flushed
122+
# servlet container auto handle chunking when response is flushed
118123
# (and Content-Length headers has not been set) :
119124
next if key == TRANSFER_ENCODING && skip_encoding_header?(value)
120125
# NOTE: effectively the same as `v.split("\n").each` which is what
@@ -147,16 +152,16 @@ def write_body(response)
147152
@body.call response.getOutputStream
148153
elsif @body.respond_to?(:to_path) # send_file
149154
send_file @body.to_path, response
150-
elsif @body.respond_to?(:to_channel) &&
155+
elsif @body.respond_to?(:to_channel) &&
151156
! object_polluted_with_anyio?(@body, :to_channel)
152157
body = @body.to_channel # so that we close the channel
153158
transfer_channel body, response.getOutputStream
154-
elsif @body.respond_to?(:to_inputstream) &&
159+
elsif @body.respond_to?(:to_inputstream) &&
155160
! object_polluted_with_anyio?(@body, :to_inputstream)
156161
body = @body.to_inputstream # so that we close the stream
157162
body = Channels.newChannel(body) # closing the channel closes the stream
158163
transfer_channel body, response.getOutputStream
159-
elsif @body.respond_to?(:body_parts) && @body.body_parts.respond_to?(:to_channel) &&
164+
elsif @body.respond_to?(:body_parts) && @body.body_parts.respond_to?(:to_channel) &&
160165
! object_polluted_with_anyio?(@body.body_parts, :to_channel)
161166
# ActionDispatch::Response "raw" body access in case it's a File
162167
body = @body.body_parts.to_channel # so that we close the channel
@@ -178,44 +183,43 @@ def write_body(response)
178183
# HACK: deal with objects that don't comply with Rack specification
179184
@body = [ @body.to_s ]
180185
retry
181-
rescue NativeException => e
182-
# Don't needlessly raise errors because of client abort exceptions
183-
raise unless e.cause.toString =~ /(clientabortexception|broken pipe)/i
186+
rescue java.io.IOException => e
187+
raise e if ! client_abort_exception?(e) || ! self.class.swallow_client_abort?
184188
ensure
185189
@body.close if @body.respond_to?(:close)
186190
body && body.close rescue nil
187191
end
188192
end
189193

190194
protected
191-
195+
192196
# @return [true, false] whether a chunked encoding is detected
193197
def chunked?
194198
return @chunked unless @chunked.nil?
195199
@chunked = !! ( @headers && @headers[TRANSFER_ENCODING] == 'chunked' )
196200
end
197-
201+
198202
# @return [true, false] whether output (body) should be flushed after each
199203
# written (yielded) line
200204
# @see #chunked?
201205
def flush?
202206
chunked? || ! ( @headers && @headers['Content-Length'] )
203207
end
204-
208+
205209
# Whether de-chunking (a chunked Rack response) should be performed.
206210
# @see JRuby::Rack::Response#dechunk?
207211
# @see #chunked?
208212
def dechunk?
209213
self.class.dechunk? && chunked?
210214
end
211-
215+
212216
# Sends a file when a Rails/Rack file response (`body.to_path`) is detected.
213217
# This allows for potential application server overrides when file streaming.
214218
# By default JRuby-Rack will stream the file using a (native) file channel.
215-
#
219+
#
216220
# @param path the file path
217221
# @param response the response environment
218-
#
222+
#
219223
# @note That this is not related to `Rack::Sendfile` support, since if you
220224
# have configured *sendfile.type* (e.g. to Apache's "X-Sendfile") this part
221225
# would not have been executing at all.
@@ -229,18 +233,22 @@ def send_file(path, response)
229233
input.close rescue nil
230234
end
231235
end
232-
236+
233237
private
234-
238+
239+
def client_abort_exception?(ioe)
240+
ioe.inspect =~ /(ClientAbortException|EofException|broken pipe)/i
241+
end
242+
235243
def skip_encoding_header?(value)
236244
value == 'chunked' && @@dechunk != false
237245
end
238-
246+
239247
def write_body_dechunked(output_stream)
240248
# NOTE: due Rails 3.2 stream-ed rendering http://git.io/ooCOtA#L223
241249
# Only required if the patch at jruby/rack/chunked.rb is not applied ...
242250
term = "\r\n"; tail = "0#{term}#{term}".freeze
243-
# we assume no support here for chunk-extensions e.g.
251+
# we assume no support here for chunk-extensions e.g.
244252
# chunk = chunk-size [ chunk-extension ] CRLF chunk-data CRLF
245253
# no need to be handled - we simply unwrap what Rails chunked :
246254
chunk = /^([0-9a-fA-F]+)#{Regexp.escape(term)}(.+)#{Regexp.escape(term)}/mo
@@ -259,7 +267,7 @@ def write_body_dechunked(output_stream)
259267
output_stream.flush
260268
end
261269
end
262-
270+
263271
def transfer_channel(channel, output_stream)
264272
output_channel = Channels.newChannel output_stream
265273
if channel.respond_to?(:transfer_to) && channel_chunk_size # FileChannel
@@ -280,11 +288,11 @@ def transfer_channel(channel, output_stream)
280288
end
281289
end
282290
end
283-
291+
284292
# Fixnum should not have this method, and it shouldn't be on Object
285293
@@object_polluted = ( Fixnum.method(:to_channel).owner == Object ) rescue nil # :nodoc
286-
287-
# See http://bugs.jruby.org/5444 - we need to account for pre-1.6 JRuby
294+
295+
# See http://bugs.jruby.org/5444 - we need to account for pre-1.6 JRuby
288296
# where Object was polluted with #to_channel ( by IOJavaAddions.AnyIO )
289297
def object_polluted_with_anyio?(obj, meth) # :nodoc
290298
@@object_polluted && begin
@@ -295,7 +303,7 @@ def object_polluted_with_anyio?(obj, meth) # :nodoc
295303
false
296304
end
297305
end
298-
306+
299307
end
300308
end
301309
end

src/spec/java/org/jruby/rack/mock/DelegatingServletOutputStream.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ public class DelegatingServletOutputStream extends ServletOutputStream {
3737

3838
private final OutputStream targetStream;
3939

40-
4140
/**
4241
* Create a DelegatingServletOutputStream for the given target stream.
4342
* @param targetStream the target stream (never <code>null</code>)
@@ -53,16 +52,17 @@ public final OutputStream getTargetStream() {
5352
return this.targetStream;
5453
}
5554

56-
5755
public void write(int b) throws IOException {
5856
this.targetStream.write(b);
5957
}
6058

59+
@Override
6160
public void flush() throws IOException {
6261
super.flush();
6362
this.targetStream.flush();
6463
}
6564

65+
@Override
6666
public void close() throws IOException {
6767
super.close();
6868
this.targetStream.close();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2014 kares.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package org.jruby.rack.mock.fail;
25+
26+
import java.io.IOException;
27+
28+
/**
29+
* <code>org.apache.catalina.connector.ClientAbortException</code>
30+
*
31+
* @author kares
32+
*/
33+
public final class ClientAbortException extends IOException {
34+
35+
public ClientAbortException() {
36+
super();
37+
}
38+
39+
public ClientAbortException(String message) {
40+
super(message);
41+
}
42+
43+
public ClientAbortException(Throwable cause) {
44+
super(cause);
45+
}
46+
47+
public ClientAbortException(String message, Throwable cause) {
48+
super(message, cause);
49+
}
50+
51+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2014 kares.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package org.jruby.rack.mock.fail;
25+
26+
import java.io.EOFException;
27+
28+
/**
29+
* <code>org.mortbay.jetty.EofException</code>
30+
*
31+
* @author kares
32+
*/
33+
public class EofException extends EOFException {
34+
35+
public EofException() {
36+
super();
37+
}
38+
39+
public EofException(String s) {
40+
super(s);
41+
}
42+
43+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2014 kares.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package org.jruby.rack.mock.fail;
25+
26+
import java.io.IOException;
27+
import org.jruby.rack.mock.MockHttpServletResponse;
28+
29+
/**
30+
* @author kares
31+
*/
32+
public class FailingHttpServletResponse extends MockHttpServletResponse {
33+
34+
private FailingServletOutputStream outputStream;
35+
36+
@Override
37+
public FailingServletOutputStream getOutputStream() {
38+
if (outputStream == null) {
39+
outputStream = new FailingServletOutputStream(new IOException("Broken pipe"));
40+
}
41+
return outputStream;
42+
}
43+
44+
public void setOutputStream(FailingServletOutputStream outputStream) {
45+
this.outputStream = outputStream;
46+
}
47+
48+
public void setFailure(IOException failure) {
49+
getOutputStream().setFailure(failure);
50+
}
51+
52+
}

0 commit comments

Comments
 (0)