Skip to content

Commit

Permalink
Fixes #1659: Do not try to read response body if request was HEAD
Browse files Browse the repository at this point in the history
  • Loading branch information
lbguilherme committed Oct 1, 2015
1 parent 9e4dd4b commit acd0b6a
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 10 deletions.
14 changes: 14 additions & 0 deletions spec/std/http/client_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ module HTTP
typeof(Client.get(URI.parse("http://www.example.com")))
typeof(Client.get("http://www.example.com"))

it "doesn't read the body if request was HEAD" do
resp_get = TestServer.open("localhost", 8080, 0) do
client = Client.new("localhost", 8080)
break client.get("/")
end

TestServer.open("localhost", 8080, 0) do
client = Client.new("localhost", 8080)
resp_head = client.head("/")
resp_head.headers.should eq(resp_get.headers)
resp_head.body.should eq("")
end
end

it "raises if URI is missing scheme" do
expect_raises(ArgumentError) do
HTTP::Client.get "www.example.com"
Expand Down
10 changes: 10 additions & 0 deletions spec/std/http/response_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ module HTTP
end
end

it "parses response ignoring body" do
response = Response.from_io(StringIO.new("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhelloworld"), true)
response.version.should eq("HTTP/1.1")
response.status_code.should eq(200)
response.status_message.should eq("OK")
response.headers["content-type"].should eq("text/plain")
response.headers["content-length"].should eq("5")
response.body.should eq("")
end

it "doesn't sets content length for 1xx, 204 or 304" do
[100, 101, 204, 304].each do |status|
response = Response.new(status)
Expand Down
4 changes: 2 additions & 2 deletions src/http/client/client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class HTTP::Client
request.headers["User-agent"] ||= "Crystal"
request.to_io(socket)
socket.flush
HTTP::Response.from_io(socket).tap do |response|
HTTP::Response.from_io(socket, request.ignore_body?).tap do |response|
close unless response.keep_alive?
end
end
Expand All @@ -303,7 +303,7 @@ class HTTP::Client
request.headers["User-agent"] ||= "Crystal"
request.to_io(socket)
socket.flush
HTTP::Response.from_io(socket) do |response|
HTTP::Response.from_io(socket, request.ignore_body?) do |response|
value = yield response
response.body_io.try &.close
close unless response.keep_alive?
Expand Down
14 changes: 11 additions & 3 deletions src/http/common.cr
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
module HTTP
DATE_PATTERNS = {"%a, %d %b %Y %H:%M:%S %z", "%A, %d-%b-%y %H:%M:%S %z", "%a %b %e %H:%M:%S %Y"}

def self.parse_headers_and_body(io, mandatory_body = false)
enum BodyType
OnDemand
Prohibited
Mandatory
end

def self.parse_headers_and_body(io, body_type = BodyType::OnDemand : BodyType)
headers = Headers.new

while line = io.gets
if line == "\r\n" || line == "\n"
body = nil
if content_length = headers["Content-length"]?
if body_type.prohibited?
body = nil
elsif content_length = headers["Content-length"]?
body = FixedLengthContent.new(io, content_length.to_i)
elsif headers["Transfer-encoding"]? == "chunked"
body = ChunkedContent.new(io)
elsif mandatory_body
elsif body_type.mandatory?
body = UnknownLengthContent.new(io)
end

Expand Down
4 changes: 4 additions & 0 deletions src/http/request.cr
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class HTTP::Request
HTTP.keep_alive?(self)
end

def ignore_body?
@method == "HEAD"
end

def to_io(io)
io << @method << " " << resource << " " << @version << "\r\n"
cookies = @cookies
Expand Down
12 changes: 7 additions & 5 deletions src/http/response.cr
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,24 @@ class HTTP::Response
version == "HTTP/1.1"
end

def self.from_io(io)
from_io(io) do |response|
def self.from_io(io, ignore_body = false)
from_io(io, ignore_body) do |response|
response.consume_body_io
return response
end
end

def self.from_io(io, &block)
def self.from_io(io, ignore_body = false, &block)
line = io.gets
if line
http_version, status_code, status_message = line.split(3)
status_code = status_code.to_i
status_message = status_message.chomp
mandatory_body = mandatory_body?(status_code)
body_type = HTTP::BodyType::OnDemand
body_type = HTTP::BodyType::Mandatory if mandatory_body?(status_code)
body_type = HTTP::BodyType::Prohibited if ignore_body

HTTP.parse_headers_and_body(io, mandatory_body) do |headers, body|
HTTP.parse_headers_and_body(io, body_type) do |headers, body|
return yield new status_code, nil, headers, status_message, http_version, body
end
end
Expand Down

0 comments on commit acd0b6a

Please sign in to comment.