diff --git a/lib/protocol/http/request.rb b/lib/protocol/http/request.rb index 648fff0..60d4794 100644 --- a/lib/protocol/http/request.rb +++ b/lib/protocol/http/request.rb @@ -36,7 +36,8 @@ class Request # @parameter body [Body::Readable] The request body. # @parameter protocol [String | Array(String) | Nil] The request protocol, usually empty, but occasionally `"websocket"` or `"webtransport"`. # @parameter interim_response [Proc] A callback which is called when an interim response is received. - def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = Headers.new, body = nil, protocol = nil, interim_response = nil) + # @parameter idempotent [Boolean | Nil] An override for {#idempotent?}, or `nil` to use the default method-based heuristic. + def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = Headers.new, body = nil, protocol = nil, interim_response = nil, idempotent: nil) @scheme = scheme @authority = authority @method = method @@ -46,6 +47,7 @@ def initialize(scheme = nil, authority = nil, method = nil, path = nil, version @body = body @protocol = protocol @interim_response = interim_response + @idempotent = idempotent end # @attribute [String] the request scheme, usually `"http"` or `"https"`. @@ -75,6 +77,9 @@ def initialize(scheme = nil, authority = nil, method = nil, path = nil, version # @attribute [Proc] a callback which is called when an interim response is received. attr_accessor :interim_response + # @attribute [Boolean | Nil] an override for {#idempotent?}, or `nil` to use the default method-based heuristic. + attr_accessor :idempotent + # A request that is generated by a server, may choose to include the peer (address) associated with the request. It should be implemented by a sub-class. # # @returns [Peer | Nil] The peer (address) associated with the request. @@ -124,16 +129,21 @@ def connect? # @parameter path [String] The path, e.g. `"/index.html"`, `"/search?q=hello"`, etc. # @parameter headers [Hash] The headers, e.g. `{"accept" => "text/html"}`, etc. # @parameter body [String | Array(String) | Body::Readable] The body, e.g. `"Hello, World!"`, etc. See {Body::Buffered.wrap} for more information about . - def self.[](method, path = nil, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil, interim_response: nil) + # @parameter idempotent [Boolean | Nil] Override the default idempotency heuristic. + def self.[](method, path = nil, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil, interim_response: nil, idempotent: nil) path = path&.to_s body = Body::Buffered.wrap(body) headers = Headers[headers] - self.new(scheme, authority, method, path, nil, headers, body, protocol, interim_response) + self.new(scheme, authority, method, path, nil, headers, body, protocol, interim_response, idempotent: idempotent) end # Whether the request can be replayed without side-effects. def idempotent? + unless @idempotent.nil? + return @idempotent + end + @method != Methods::POST && (@body.nil? || @body.empty?) end diff --git a/test/protocol/http/request.rb b/test/protocol/http/request.rb index a4e8e40..fb1e872 100644 --- a/test/protocol/http/request.rb +++ b/test/protocol/http/request.rb @@ -128,6 +128,10 @@ expect(request).to be(:idempotent?) end + it "defaults idempotent to nil" do + expect(request.idempotent).to be_nil + end + it "should have a string representation" do expect(request.to_s).to be == "http://localhost: GET /index.html HTTP/1.0" end @@ -141,6 +145,42 @@ end end + with "simple POST request" do + let(:request) {subject.new("http", "localhost", "POST", "/submit", "HTTP/1.1", headers, nil)} + + it "should not be idempotent" do + expect(request).not.to be(:idempotent?) + end + + with "idempotent: true" do + let(:request) {subject.new("http", "localhost", "POST", "/submit", "HTTP/1.1", headers, nil, nil, nil, idempotent: true)} + + it "should be idempotent" do + expect(request).to be(:idempotent?) + end + end + end + + with "idempotent: false" do + let(:request) {subject.new("http", "localhost", "GET", "/index.html", "HTTP/1.0", headers, body, nil, nil, idempotent: false)} + + it "should not be idempotent" do + expect(request).not.to be(:idempotent?) + end + end + + with ".[] with idempotent: true" do + let(:request) {subject["POST", "/submit", idempotent: true]} + + it "should be idempotent" do + expect(request).to be(:idempotent?) + end + + it "should expose the idempotent attribute" do + expect(request.idempotent).to be == true + end + end + with "interim response" do let(:request) {subject.new("http", "localhost", "GET", "/index.html", "HTTP/1.0", headers, body)}