Skip to content

Commit 8068027

Browse files
authored
Merge pull request #479 from chadlwilson/1.2-backport-fixes
1.2: backport minor Rack 2.2-era spec compatibility fixes
2 parents 02be79d + 32fa9d5 commit 8068027

9 files changed

Lines changed: 97 additions & 33 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
- fix: ensure `rack.` internal headers are stripped in responses
44
- chore: remove ancient dead Rails 2-era adapter code
5+
- fix: ensure ErrorApp does not mutate shared headers constant
6+
- fix: JRuby::Rack::Input#read(0) should return an empty string
7+
- fix: close the original body when ShowStatus replaces it
8+
- fix: detect Transfer-Encoding/Content-Length headers case-insensitively
59

610
## 1.2.8
711

‎src/main/java/org/jruby/rack/ext/Input.java‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ else if ( arg instanceof RackEnvironment ) {
126126
@JRubyMethod()
127127
public IRubyObject gets(final ThreadContext context) {
128128
try {
129-
final int NEWLINE = 10;
130-
final byte[] bytes = readUntil(NEWLINE, 0);
129+
final byte[] bytes = readUntil('\n', 0);
131130
if ( bytes != null ) {
132131
return context.runtime.newString(new ByteList(bytes, false));
133132
}
@@ -155,14 +154,19 @@ public IRubyObject gets(final ThreadContext context) {
155154
*/
156155
@JRubyMethod(optional = 2)
157156
public IRubyObject read(final ThreadContext context, final IRubyObject[] args) {
158-
int readLen = 0;
159-
if ( args.length > 0 ) {
157+
int readLen = 0; boolean readAll = true;
158+
if ( args.length > 0 && ! args[0].isNil() ) {
160159
long len = args[0].convertToInteger("to_i").getLongValue();
161160
readLen = (int) Math.min(len, Integer.MAX_VALUE);
161+
readAll = false;
162162
}
163163
final RubyString buffer = args.length > 1 ? args[1].asString() : null;
164+
if ( ! readAll && readLen <= 0 ) { // like IO#read - read(0) returns "" (not all data)
165+
if ( buffer != null ) { buffer.clear(); return buffer; }
166+
return RubyString.newEmptyString(context.runtime);
167+
}
164168
try {
165-
final byte[] bytes = readUntil(MATCH_NONE, readLen);
169+
final byte[] bytes = readUntil(MATCH_NONE, readAll ? 0 : readLen);
166170
if ( bytes != null ) {
167171
if ( buffer != null ) {
168172
buffer.clear();
@@ -176,7 +180,7 @@ public IRubyObject read(final ThreadContext context, final IRubyObject[] args) {
176180
}
177181
return context.runtime.newString(new ByteList(bytes, false));
178182
}
179-
return readLen > 0 ? context.nil : RubyString.newEmptyString(context.runtime);
183+
return readAll ? RubyString.newEmptyString(context.runtime) : context.nil;
180184
}
181185
catch (IOException e) {
182186
throw ExceptionUtils.newIOError(context.runtime, e);

‎src/main/java/org/jruby/rack/ext/Logger.java‎

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -448,18 +448,12 @@ public IRubyObject format_severity(final ThreadContext context, final IRubyObjec
448448
return RubyString.newStringShared(context.runtime, formatSeverity(severity));
449449
}
450450

451-
private static final ByteList FORMATTED_DEBUG =
452-
new ByteList(new byte[] { 'D','E','B','U','G' }, false);
453-
private static final ByteList FORMATTED_INFO =
454-
new ByteList(new byte[] { 'I','N','F','O' }, false);
455-
private static final ByteList FORMATTED_WARN =
456-
new ByteList(new byte[] { 'W','A','R','N' }, false);
457-
private static final ByteList FORMATTED_ERROR =
458-
new ByteList(new byte[] { 'E','R','R','O','R' }, false);
459-
private static final ByteList FORMATTED_FATAL =
460-
new ByteList(new byte[] { 'F','A','T','A','L' }, false);
461-
private static final ByteList FORMATTED_ANY =
462-
new ByteList(new byte[] { 'A','N','Y' }, false);
451+
private static final ByteList FORMATTED_DEBUG = ByteList.create("DEBUG");
452+
private static final ByteList FORMATTED_INFO = ByteList.create("INFO");
453+
private static final ByteList FORMATTED_WARN = ByteList.create("WARN");
454+
private static final ByteList FORMATTED_ERROR = ByteList.create("ERROR");
455+
private static final ByteList FORMATTED_FATAL = ByteList.create("FATAL");
456+
private static final ByteList FORMATTED_ANY = ByteList.create("ANY");
463457

464458
private static ByteList formatSeverity(final int severity) {
465459
switch ( severity) {

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

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ public IRubyObject write_headers(final ThreadContext context, final IRubyObject
362362
return context.nil;
363363
}
364364

365-
private static final ByteList NEW_LINE = new ByteList(new byte[] { '\n' }, false);
365+
private static final ByteList NEW_LINE = ByteList.create("\n");
366366

367367
protected void writeHeaders(final RackResponseEnvironment response) {
368368
this.headers.visitAll(currentContext(), new RubyHash.Visitor() { // headers.each { |key, val| }
@@ -386,7 +386,7 @@ public void visit(final IRubyObject key, final IRubyObject val) {
386386
} // else will do addHeader
387387
}
388388

389-
if ( name.equals("Transfer-Encoding") ) {
389+
if ( name.equalsIgnoreCase("Transfer-Encoding") ) {
390390
if ( skipEncodingHeader(val) ) return;
391391
}
392392

@@ -566,9 +566,8 @@ public IRubyObject chunked_p(final ThreadContext context) {
566566
return context.runtime.newBoolean( isChunked() );
567567
}
568568

569-
private static final ByteList TRANSFER_ENCODING = new ByteList(
570-
new byte[] { 'T','r','a','n','s','f','e','r','-','E','n','c','o','d','i','n','g' },
571-
false);
569+
private static final ByteList TRANSFER_ENCODING = ByteList.create("Transfer-Encoding");
570+
private static final ByteList TRANSFER_ENCODING_LOWER = ByteList.create("transfer-encoding");
572571

573572
private Boolean chunked;
574573

@@ -578,15 +577,26 @@ public IRubyObject chunked_p(final ThreadContext context) {
578577
public boolean isChunked() {
579578
if ( chunked != null ) return chunked;
580579
if ( this.headers != null ) {
581-
final RubyString key = RubyString.newString(getRuntime(), TRANSFER_ENCODING);
582-
final IRubyObject value = this.headers.callMethod("[]", key);
580+
final IRubyObject value = getHeaderValue(TRANSFER_ENCODING, TRANSFER_ENCODING_LOWER);
583581
if ( value instanceof RubyString ) {
584582
return chunked = ( (RubyString) value ).getByteList().equal(CHUNKED);
585583
}
586584
}
587585
return chunked = Boolean.FALSE;
588586
}
589587

588+
/**
589+
* Rack does not mandate response header name casing - apps might use the
590+
* conventional Capitalized-Names or (Rack 3.x style) lower-case names.
591+
*/
592+
private IRubyObject getHeaderValue(final ByteList canonicalName, final ByteList lowerCaseName) {
593+
IRubyObject value = this.headers.callMethod("[]", RubyString.newString(getRuntime(), canonicalName));
594+
if ( value.isNil() ) {
595+
value = this.headers.callMethod("[]", RubyString.newString(getRuntime(), lowerCaseName));
596+
}
597+
return value;
598+
}
599+
590600
/**
591601
* @return whether de-chunking (a chunked Rack response) should be performed
592602
*/
@@ -599,19 +609,17 @@ public IRubyObject flush_p(final ThreadContext context) {
599609
return context.runtime.newBoolean( doFlush() );
600610
}
601611

602-
private static final ByteList CONTENT_LENGTH = new ByteList(
603-
new byte[] { 'C','o','n','t','e','n','t','-','L','e','n','g','t','h' },
604-
false);
612+
private static final ByteList CONTENT_LENGTH = ByteList.create("Content-Length");
613+
private static final ByteList CONTENT_LENGTH_LOWER = ByteList.create("content-length");
605614

606615
/**
607616
* @return whether output (body) should be flushed after each written line
608617
*/
609618
protected boolean doFlush() {
610619
if ( isChunked() ) return true;
611620
if ( this.headers != null ) {
612-
final RubyString key = RubyString.newString(getRuntime(), CONTENT_LENGTH);
613-
final IRubyObject value = this.headers.callMethod("[]", key);
614-
return value.isNil(); // does not have a Content-Length header
621+
// does not have a Content-Length header :
622+
return getHeaderValue(CONTENT_LENGTH, CONTENT_LENGTH_LOWER).isNil();
615623
}
616624
return false;
617625
}
@@ -645,7 +653,7 @@ protected boolean isClientAbortException(final Exception ioe) {
645653
return false;
646654
}
647655

648-
private static final ByteList CHUNKED = new ByteList(new byte[] { 'c','h','u','n','k','e','d' }, false);
656+
private static final ByteList CHUNKED = ByteList.create("chunked");
649657

650658
private boolean skipEncodingHeader(final IRubyObject value) {
651659
if ( dechunk == Boolean.FALSE ) return false;

‎src/main/ruby/jruby/rack/error_app.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def map_error_code(exc)
105105
end
106106
end
107107

108-
def respond(status = nil, body = nil, headers = DEFAULT_HEADERS)
108+
def respond(status = nil, body = nil, headers = DEFAULT_HEADERS.dup)
109109
status ||= DEFAULT_RESPONSE_CODE
110110
body += "\n" if body
111111
headers['Content-Type'] = "text/plain" unless headers.key?('Content-Type')

‎src/main/ruby/jruby/rack/error_app/show_status.rb‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def call(env)
2121
detail = env['rack.showstatus.detail']
2222
# client or server error, or explicit message
2323
if (status.to_i >= 400 && empty) || detail
24+
# SPEC: when replacing the body the original one needs to be closed :
25+
body.close if body.respond_to?(:close)
2426
# required erb template variables (captured with binding) :
2527
request = req = ::Rack::Request.new(env); request && req # avoid un-used warning
2628
message = ::Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s

‎src/spec/ruby/jruby/rack/error_app_spec.rb‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@
8282
end
8383
end
8484

85+
it "returns a fresh headers hash for each response" do
86+
init_exception
87+
response1 = error_app.call(@env)
88+
response1[1]['X-Polluted'] = 'leaked'
89+
90+
response2 = error_app.call(@env)
91+
expect(response2[1]).to_not include 'X-Polluted'
92+
expect(JRuby::Rack::ErrorApp::DEFAULT_HEADERS).to be_empty
93+
end
94+
8595
it spec = "still serves when retrieving exception's message fails" do
8696
@env['HTTP_ACCEPT'] = '*/*'
8797
@env[JRuby::Rack::ErrorApp::EXCEPTION] = InitException.new spec
@@ -133,6 +143,18 @@ def message
133143
expect(@env['rack.showstatus.detail']).to be false
134144
end
135145

146+
it "closes the original body when replacing it with the rendered template" do
147+
body = double('body', :each => nil)
148+
expect(body).to receive(:close)
149+
150+
app = lambda { |env| [ 500, {}, body ] }
151+
show_status = JRuby::Rack::ErrorApp::ShowStatus.new(app)
152+
@env['HTTP_ACCEPT'] = '*/*'
153+
154+
response = show_status.call(@env)
155+
expect(response[2][0]).to include 'Internal Server Error'
156+
end
157+
136158
it "with response < 400 and 'rack.showstatus.detail' set to false does not render exception" do
137159
@env['HTTP_ACCEPT'] = '*/*'; init_exception
138160
@env['rack.showstatus.detail'] = false

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ class << value
137137
expect(response.chunked?).to be true
138138
end
139139

140+
it "detects a chunked response with a lower-case transfer-encoding header" do
141+
headers = { "transfer-encoding" => "chunked" }
142+
response = JRuby::Rack::Response.new [200, headers, ['body']]
143+
# NOTE: servlet container auto handle chunking when flushed no need to set :
144+
expect(servlet_response).not_to receive(:addHeader).with("transfer-encoding", "chunked")
145+
response.write_headers(response_environment)
146+
expect(response.chunked?).to be true
147+
end
148+
140149
describe "#write_body" do
141150

142151
let(:stream) do
@@ -304,6 +313,16 @@ class << value
304313
response.write_body(response_environment)
305314
end
306315

316+
it "does not flush the body when lower-case content-length set" do
317+
headers = { "content-length" => 10 }
318+
response = JRuby::Rack::Response.new [200, headers, ['hello', 'there']]
319+
320+
response.write_headers(response_environment)
321+
322+
expect(stream).to receive(:flush).never
323+
response.write_body(response_environment)
324+
end
325+
307326
it "writes the body to the servlet response" do
308327
response = JRuby::Rack::Response.new [200, {}, ['1', '2', '3']]
309328

‎src/spec/ruby/rack/input_spec.rb‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ def it_should_behave_like_rack_input
3939
expect(input.read(16)).to eq "hello\r\ngoodbye"
4040
end
4141

42+
it "should return an empty string for read(0) without consuming input" do
43+
expect(input.read(0)).to eq ""
44+
expect(input.read).to eq "hello\r\ngoodbye"
45+
end
46+
47+
it "should replace buffer contents with an empty string for read(0, buffer)" do
48+
buf = "cruft"
49+
expect(input.read(0, buf)).to eq ""
50+
expect(buf).to eq ""
51+
end
52+
4253
it "should read into a provided buffer" do
4354
buf = ""
4455
input.read(nil, buf)

0 commit comments

Comments
 (0)