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
22 changes: 22 additions & 0 deletions lib/protocol/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,28 @@ def idempotent?
return false
end

# Rewind the request body so it can be sent again.
# @returns [Boolean] Whether the request body was rewound.
def rewind!
if body = @body
return body.rewind
end

# Requests without a body can be sent again immediately.
return true
end

# Prepare the request body to be sent again, if it is safe to retry.
# @returns [Boolean] Whether the request was prepared for retry.
def retry!
# Only idempotent request methods can be retried safely.
if @method == Methods::POST || @method == Methods::PATCH || @method == Methods::CONNECT
return false
end

return self.rewind!
end

# Convert the request to a hash, suitable for serialization.
#
# @returns [Hash] The request as a hash.
Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## Unreleased

- Add `Protocol::HTTP::Request#rewind!` and `#retry!` for preparing requests to be sent again.

## v0.63.0

- Add support for the HTTP `QUERY` method.
Expand Down
121 changes: 121 additions & 0 deletions test/protocol/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,127 @@
end
end

with "PATCH request without a body" do
let(:request) {subject["PATCH", "/resource"]}

it "should be idempotent" do
expect(request).to be(:idempotent?)
end
end

with "#rewind!" do
it "allows requests without a body" do
expect(request.rewind!).to be == true
end

with "request with a rewindable body" do
let(:request) {subject["POST", "/submit", body: "content"]}

it "rewinds the body" do
expect(request.body.read).to be == "content"

expect(request.rewind!).to be == true
expect(request.body.read).to be == "content"
end
end

with "request with a non-rewindable body" do
let(:body) {Protocol::HTTP::Body::Readable.new}
let(:request) {subject.new(nil, nil, "PUT", "/resource", nil, headers, body)}

it "does not rewind" do
expect(request.rewind!).to be == false
end
end

with "request with a consumed non-rewindable body" do
let(:body) do
Class.new(Protocol::HTTP::Body::Readable) do
def initialize
@chunk = "content"
end

def read
chunk = @chunk
@chunk = nil
return chunk
end

def empty?
@chunk.nil?
end
end.new
end

let(:request) {subject.new(nil, nil, "PUT", "/resource", nil, headers, body)}

it "does not treat empty as rewindable" do
expect(request.body.read).to be == "content"
expect(request.body).to be(:empty?)

expect(request.rewind!).to be == false
end
end

with "request with an empty non-rewindable body" do
let(:body) do
Class.new(Protocol::HTTP::Body::Readable) do
def empty?
true
end
end.new
end

let(:request) {subject.new(nil, nil, "PUT", "/resource", nil, headers, body)}

it "does not rewind" do
expect(request.rewind!).to be == false
end
end
end

with "#retry!" do
it "allows idempotent requests without a body" do
expect(request.retry!).to be == true
end

with "idempotent request with a rewindable body" do
let(:request) {subject["PUT", "/resource", body: "content"]}

it "allows retry and rewinds the body" do
expect(request.body.read).to be == "content"

expect(request.retry!).to be == true
expect(request.body.read).to be == "content"
end
end

with "idempotent request with a non-rewindable body" do
let(:body) {Protocol::HTTP::Body::Readable.new}
let(:request) {subject.new(nil, nil, "PUT", "/resource", nil, headers, body)}

it "does not allow retry" do
expect(request.retry!).to be == false
end
end

with "non-idempotent request" do
let(:request) {subject["POST", "/submit"]}

it "does not allow retry" do
expect(request.retry!).to be == false
end
end

with "PATCH request" do
let(:request) {subject["PATCH", "/resource"]}

it "does not allow retry" do
expect(request.retry!).to be == false
end
end
end

it "should have a string representation" do
expect(request.to_s).to be == "http://localhost: GET /index.html HTTP/1.0"
end
Expand Down
Loading