diff --git a/lib/protocol/http/request.rb b/lib/protocol/http/request.rb index c9593cf..325960b 100644 --- a/lib/protocol/http/request.rb +++ b/lib/protocol/http/request.rb @@ -153,6 +153,34 @@ def idempotent? return false 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 + + # Requests without a body can be sent again immediately. + if body = @body + # Empty, non-rewindable bodies have nothing left to send. + if body.empty? && !body.rewindable? + return true + end + + # Non-empty bodies must be rewindable so the same data can be sent again. + if !body.rewindable? + return false + end + + if !body.rewind + return false + end + end + + return true + end + # Convert the request to a hash, suitable for serialization. # # @returns [Hash] The request as a hash. diff --git a/test/protocol/http/request.rb b/test/protocol/http/request.rb index 9958c6b..de31388 100644 --- a/test/protocol/http/request.rb +++ b/test/protocol/http/request.rb @@ -152,6 +152,56 @@ 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 "#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