Repository: rage-rb/rage
Pull request #123: Enhance Request
Author: login: aaoafk
Created: 2025-01-16T20:54:56Z | Merged: 2025-03-17T17:00:27Z

hello,

i think this should mostly work unless i've missed something.

the only issue right now is maybe `remote_ip` needs to be thought through a little more and the current method of checking the http action does not verify against `HTTP_METHODS` I just need to port that underscore method over or just do something else.

lmk your initial thoughts


Diff:
diff --git a/lib/rage/errors.rb b/lib/rage/errors.rb
index 6a64aac2..5bb6f509 100644
--- a/lib/rage/errors.rb
+++ b/lib/rage/errors.rb
@@ -4,4 +4,10 @@ class BadRequest < StandardError
 
   class RouterError < StandardError
   end
+
+  class UnknownHTTPMethod < StandardError
+  end
+
+  class InvalidCustomProxy < StandardError
+  end
 end
diff --git a/lib/rage/request.rb b/lib/rage/request.rb
index 4bf708aa..2def7d36 100644
--- a/lib/rage/request.rb
+++ b/lib/rage/request.rb
@@ -1,13 +1,151 @@
 # frozen_string_literal: true
 
 require "time"
+require "set" # required for ruby 3.1
 
 class Rage::Request
+  # regexp to match against a ip address
+  IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  # Set data structure of all RFC defined HTTP headers
+  HTTP_METHODS_SET = (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).to_set
+
   # @private
   def initialize(env)
     @env = env
   end
 
+  # Checks if the request was made using TLS/SSL which is if http or https protocol is used inside the URL.
+  # @return [Boolean] true if the request is TLS/SSL, false otherwise
+  def ssl?
+    rack_request.ssl?
+  end
+
+  # Returns 'https://' if this was an HTTPS request and 'http://' otherwise
+  # @return [String] 'https://' if this was an HTTP request over SSL/TLS or 'http://' if it was an HTTP request
+  def protocol
+    ssl? ? "https://" : "http://"
+  end
+
+  # Gets the hostname from the request
+  # @return [String] the hostname
+  def host
+    rack_request.host
+  end
+
+  # Gets the port number from the request
+  # @return [Integer] the port number
+  def port
+    rack_request.port
+  end
+
+  # Gets the query string from the request
+  # @return [String] the query string (empty string if no query)
+  def query_string
+    rack_request.query_string
+  end
+
+  # Gets the environment hash
+  # @return [Hash] the environment variables hash
+  def env
+    @env
+  end
+
+  # Gets the specified HTTP header value
+  # @param name [String] the name of the header to retrieve
+  # @return [String, nil] the header value or nil if not found
+  def get_header(name)
+    rack_request.get_header(name)
+  end
+
+  # Checks if the request uses GET method
+  # @return [Boolean] true if GET request
+  def get?
+    rack_request.get?
+  end
+
+  # Checks if the request uses POST method
+  # @return [Boolean] true if POST request
+  def post?
+    rack_request.post?
+  end
+
+  # Checks if the request uses PATCH method
+  # @return [Boolean] true if PATCH request
+  def patch?
+    rack_request.patch?
+  end
+
+  # Checks if the request uses PUT method
+  # @return [Boolean] true if PUT request
+  def put?
+    rack_request.put?
+  end
+
+  # Checks if the request uses DELETE method
+  # @return [Boolean] true if DELETE request
+  def delete?
+    rack_request.delete?
+  end
+
+  # Checks if the request uses HEAD method
+  # @return [Boolean] true if HEAD request
+  def head?
+    rack_request.head?
+  end
+
+  # Gets the full request URL
+  # @return [String] complete URL including protocol, host, path, and query
+  def url
+    rack_request.url
+  end
+
+  # Gets the request path
+  # @return [String] the path component of the URL
+  # @example
+  #   request.path # => "/users"
+  def path
+    rack_request.path
+  end
+
+  # Gets the full path including query string
+  # @return [String] path with query string (if present)
+  # @example
+  #   request.fullpath # => "/users?show_archived=true"
+  def fullpath
+    rack_request.fullpath
+  end
+
+  # Gets the User-Agent header value
+  # @return [String, nil] the user agent string or nil
+  # @example
+  #  request.user_agent # => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
+  def user_agent
+    rack_request.user_agent
+  end
+
+  # Gets the content type of the request
+  # @return [String, nil] the MIME type of the request body
+  def format
+    rack_request.content_type
+  end
+
   # Get the request headers.
   # @example
   #   request.headers["Content-Type"] # => "application/json"
@@ -37,46 +175,59 @@ def fresh?(etag:, last_modified:)
     )
   end
 
-  # Returns the full URL of the request.
-  # @example
-  #   request.url # => "https://example.com/users?show_archived=true"
-  def url
-    scheme = @env["rack.url_scheme"]
-    host = @env["SERVER_NAME"]
-    port = @env["SERVER_PORT"]
-    path = @env["PATH_INFO"]
-    query_string = @env["QUERY_STRING"]
+  # Returns the domain of the request.
+  # @param tld_length [Integer] specify tld_length levels down into domain tree
+  # @example Consider a URL like: "example.foo.gov"
+  #   request.domain => "foo.gov"
+  #   request.domain(0) => "gov"
+  #   request.domain(2) => "example.foo.gov"
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
 
-    port_part = (scheme == "http" && port == "80") || (scheme == "https" && port == "443") ? "" : ":#{port}"
-    query_part = query_string.empty? ? "" : "?#{query_string}"
+  # Gets the HTTP method of the request. If the client is using `Rack::MethodOverride`
+  # middleware then the `X-HTTP-Method-Override` header is checked before `REQUEST_METHOD`
+  # @return [String] The HTTP Method override header or the request method header
+  def method
+    check_method(get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD"))
+  end
 
-    "#{scheme}://#{host}#{port_part}#{path}#{query_part}"
+  # Returns the unique request ID. By default, this ID is internally generated, and all log entries created during the request
+  # are tagged with it. Alternatively, you can use the {Rage::RequestId} middleware to derive the ID from the `X-Request-Id` header.
+  def request_id
+    @env["rage.request_id"]
   end
 
-  # Returns the path of the request.
-  # @example
-  #   request.path # => "/users"
-  def path
-    @env["PATH_INFO"]
+  alias_method :uuid, :request_id
+
+  private
+
+  def rack_request
+    @rack_request ||= Rack::Request.new(@env)
   end
 
-  # Returns the full path including the query string.
-  # @example
-  #   request.fullpath # => "/users?show_archived=true"
-  def fullpath
-    path = @env["PATH_INFO"]
-    query_string = @env["QUERY_STRING"]
-    query_string.empty? ? path : "#{path}?#{query_string}"
+  def check_method(name)
+    http_methods_set = HTTP_METHODS_SET
+    if name
+      if http_methods_set.include?(name)
+        name
+      else
+        raise(Rage::Errors::UnknownHTTPMethod, "#{name}, accepted HTTP methods are #{http_methods_set.to_a}")
+      end
+    end
   end
 
-  # Returns the user agent of the request.
-  # @example
-  #  request.user_agent # => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
-  def user_agent
-    @env["HTTP_USER_AGENT"]
+  def extract_domain(host, tld_length)
+    extract_domain_from(host, tld_length) if named_host?(host)
   end
 
-  private
+  def extract_domain_from(host, tld_length)
+    host.split(".").last(1 + tld_length).join(".")
+  end
+
+  def named_host?(host)
+    !IP_HOST_REGEXP.match?(host)
+  end
 
   def if_none_match
     headers["HTTP_IF_NONE_MATCH"]
diff --git a/spec/rage/request_spec.rb b/spec/rage/request_spec.rb
index 57a2d948..3e349507 100644
--- a/spec/rage/request_spec.rb
+++ b/spec/rage/request_spec.rb
@@ -4,6 +4,7 @@
       "rack.upgrade?" => nil,
       "rack.upgrade" => nil,
       "rack.version" => [1, 3],
+      "rage.request_id" => "5zfcwiy09bary01l",
       "SCRIPT_NAME" => "",
       "rack.url_scheme" => "http",
       "HTTP_VERSION" => "HTTP/1.1",
@@ -22,7 +23,9 @@
       "HTTP_ACCEPT" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
       "HTTP_ACCEPT_ENCODING" => "gzip, deflate, br, zstd",
       "HTTP_ACCEPT_LANGUAGE" => "en-GB,en-US;q=0.9,en;q=0.8",
-      "HTTP_COOKIE" => "test_cookie=1"
+      "HTTP_COOKIE" => "test_cookie=1",
+      "HTTP_X_FORWARDED_FOR" => "127.0.0.1",
+      "CONTENT_TYPE" => "application/json"
     }
   end
 
@@ -44,22 +47,205 @@
     expect(request.user_agent).to eq("Mozilla/5.0 (Macintosh; ...")
   end
 
-  it "handles default ports in URL" do
-    env["SERVER_PORT"] = "80"
-    expect(request.url).to eq("http://localhost/users?show_archived=true")
+  context "when SERVER_PORT is a default port" do
+    before do
+      env["rack.url_scheme"] = "http"
+      env["SERVER_PORT"] = "80"
+      env["HTTP_HOST"] = "localhost:80"
+    end
 
-    env["rack.url_scheme"] = "https"
-    env["SERVER_PORT"] = "443"
-    expect(request.url).to eq("https://localhost/users?show_archived=true")
+    it "it omits HTTP port 80 in the URL" do
+      expect(request.url).to eq("http://localhost/users?show_archived=true")
+    end
+
+    context "and using HTTPS" do
+      before do
+        env["rack.url_scheme"] = "https"
+        env["SERVER_PORT"] = "443"
+        env["HTTP_HOST"] = "localhost:443"
+      end
+
+      it "omits the HTTPS port 443 in the URL" do
+        expect(request.url).to eq("https://localhost/users?show_archived=true")
+      end
+    end
+  end
+
+  describe "QUERY_STRING property" do
+    context "when QUERY_STRING is empty" do
+      before do
+        env["QUERY_STRING"] = ""
+      end
+
+      it "returns the path without query string" do
+        expect(request.fullpath).to eq("/users")
+      end
+    end
+    context "when QUERY_STRING is not empty" do
+      it "handles the query string property of a request" do
+        expect(request.query_string).to eq("show_archived=true")
+      end
+    end
+  end
+
+  context "when HTTP_USER_AGENT is missing" do
+    before do
+      env.delete("HTTP_USER_AGENT")
+    end
+
+    it "returns nil for user_agent" do
+      expect(request.user_agent).to be_nil
+    end
+  end
+
+  describe "host property of a request" do
+    subject { request.host }
+
+    context "with HTTP_HOST header" do
+      context "when host contains port" do
+        it { is_expected.to eq("localhost") } # uses default env
+      end
+
+      context "when host is without port" do
+        before { env["HTTP_HOST"] = "localhost" }
+        it { is_expected.to eq("localhost") }
+      end
+
+      context "when using IP address with port" do
+        before { env["HTTP_HOST"] = "127.0.0.1:8080" }
+        it { is_expected.to eq("127.0.0.1") }
+      end
+
+      context "when containing subdomains" do
+        before { env["HTTP_HOST"] = "api.foo.bar.com:443" }
+        it { is_expected.to eq("api.foo.bar.com") }
+      end
+    end
+
+    context "without HTTP_HOST header" do
+      before do
+        env.delete("HTTP_HOST")
+        env["SERVER_NAME"] = "fallback.example"
+        env["SERVER_PORT"] = "3000"
+      end
+
+      it "falls back to SERVER_NAME" do
+        expect(subject).to eq("fallback.example")
+      end
+    end
+  end
+
+  describe "domain property" do
+    context "with default HTTP_HOST" do
+      it "returns the correct domain" do
+        expect(request.domain).to eq("localhost")
+        expect(request.domain(0)).to eq("localhost")
+        expect(request.domain(2)).to eq("localhost")
+      end
+    end
+
+    context "with HTTP_HOST set to tld0.tld1.tld2" do
+      before { env["HTTP_HOST"] = "tld0.tld1.tld2" }
+
+      it "returns the correct domains for different levels" do
+        expect(request.domain(0)).to eq("tld2")
+        expect(request.domain(1)).to eq("tld1.tld2")
+        expect(request.domain(2)).to eq("tld0.tld1.tld2")
+        expect(request.domain(3)).to eq("tld0.tld1.tld2")
+      end
+    end
+  end
+
+  describe "HTTP method handling" do
+    context "with GET request" do
+      it "identifies GET method" do
+        expect(request.method).to eq("GET") # default init is GET
+        expect(request.get?).to eq(true)
+      end
+    end
+
+    context "with POST request" do
+      before do
+        env["REQUEST_METHOD"] = "POST"
+      end
+
+      it "identifies POST method" do
+        expect(request.method).to eq("POST")
+        expect(request.post?).to eq(true)
+      end
+    end
+    context "with PATCH request" do
+      before do
+        env["REQUEST_METHOD"] = "PATCH"
+      end
+      it "identifies PATCH method" do
+        expect(request.method).to eq("PATCH")
+        expect(request.patch?).to eq(true)
+      end
+    end
+    context "with PUT request" do
+      before do
+        env["REQUEST_METHOD"] = "PUT"
+      end
+
+      it "identifies PUT method" do
+        expect(request.method).to eq("PUT")
+        expect(request.put?).to eq(true)
+      end
+    end
+    context "with DELETE request" do
+      before do
+        env["REQUEST_METHOD"] = "DELETE"
+      end
+
+      it "identifies DELETE method" do
+        expect(request.method).to eq("DELETE")
+        expect(request.delete?).to eq(true)
+      end
+    end
+
+    context "with HEAD request" do
+      before do
+        env["REQUEST_METHOD"] = "HEAD"
+      end
+      it "identifies HEAD method" do
+        expect(request.method).to eq("HEAD")
+        expect(request.head?).to eq(true)
+      end
+    end
+  end
+
+  it "handles the port property of a request" do
+    expect(request.port).to eq(3000)
+  end
+
+  describe "protocol property of a request" do
+    context "with HTTP" do
+      it "returns 'http://'" do
+        expect(request.protocol).to eq("http://")
+      end
+    end
+
+    context "with HTTPS" do
+      before do
+        env["rack.url_scheme"] = "https"
+        env["SERVER_PORT"] = "443"
+      end
+      it "returns 'https://'" do
+        expect(request.protocol).to eq("https://")
+      end
+    end
+  end
+
+  it "handles the env property of a request" do
+    expect(request.env).to eq(env)
   end
 
-  it "handles empty query string in full path" do
-    env["QUERY_STRING"] = ""
-    expect(request.fullpath).to eq("/users")
+  it "handles the format property of a request" do
+    expect(request.format).to eq("application/json")
   end
 
-  it "handles missing user agent header" do
-    env.delete("HTTP_USER_AGENT")
-    expect(request.user_agent).to be_nil
+  it "returns the correct request ID" do
+    expect(request.request_id).to eq("5zfcwiy09bary01l")
   end
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6224c61c..ed03b341 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -10,6 +10,9 @@
 require_relative "support/contexts/mocked_rage_routes"
 
 RSpec.configure do |config|
+  # Uncomment the line below to enable focused mode
+  # config.filter_run focus: true
+
   # Enable flags like --only-failures and --next-failure
   config.example_status_persistence_file_path = ".rspec_status"
 


Reviews:
user: login: rsamoilov
body: I've left some comments, but this looks great!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-19T21:06:35Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:10:53Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:13:38Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:14:06Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:16:26Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-22T20:14:10Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-22T20:14:39Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-22T20:15:37Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-22T20:17:44Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-23T01:46:48Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-23T01:47:12Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-23T01:47:32Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-26T17:25:10Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-26T17:26:58Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-26T17:45:23Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-26T17:52:45Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-26T17:54:23Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-11T01:52:19Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-11T02:03:21Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-11T23:52:50Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-11T23:57:55Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-12T17:11:17Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-12T23:23:18Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T00:14:54Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-13T18:40:44Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T22:14:50Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T22:19:27Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T22:22:21Z
user: login: rsamoilov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T19:23:53Z
user: login: rsamoilov
body: Hey Steven, I've left several comments to fix the tests.

Could you also fix the [style](https://github.com/rage-rb/rage/actions/runs/13823976262/job/38732136135?pr=123) warnings? Once it's done, the PR will be good to go.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T18:58:29Z
user: login: aaoafk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T03:05:46Z
user: login: rsamoilov
body: 🚢 Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T16:59:50Z

Review comments:
diff_hunk: @@ -1,11 +1,60 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require 'forwardable'
 
 class Rage::Request
+  extend Forwardable
+  attr_accessor :rack_request
+  # get?/post?/patch?/put?/delete?/head?
+  def_delegators :@rack_request,
+                 :ssl?,
+                 :host,
+                 :port,
+                 :query,
+                 :query_string,
+                 :env,
+                 :get_header,
+                 :get?,
+                 :post?,
+                 :patch?,
+                 :put?,
+                 :delete?,
+                 :head?
+
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
+
+  HTTP_METHOD_LOOKUP = {}
+
+  # Populate the HTTP method lookup cache.
+  # TODO: Extract underscore method
+  # HTTP_METHODS.each { |method|
+  #   HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
+  # }
+
   # @private
   def initialize(env)
     @env = env
+    @rack_request = Rack::Request.new(env)
path: lib/rage/request.rb
user: login: rsamoilov
body: Interesting, so you don't actually include the `Env` and `Helpers` methods but use `Rack::Request` directly instead.
Do you think such approach is better?
created_at: 2025-01-19T20:49:44Z
author_association: MEMBER
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
path: lib/rage/request.rb
user: login: rsamoilov
body: If I'm reading this right, you are handling the `_method` parameter here.
Rage doesn't allow overriding the request method using the `_method` parameter, so checking `env["REQUEST_METHOD"]` should suffice.
created_at: 2025-01-19T20:52:35Z
author_association: MEMBER
diff_hunk: @@ -1,11 +1,60 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require 'forwardable'
 
 class Rage::Request
+  extend Forwardable
+  attr_accessor :rack_request
+  # get?/post?/patch?/put?/delete?/head?
+  def_delegators :@rack_request,
+                 :ssl?,
+                 :host,
+                 :port,
+                 :query,
path: lib/rage/request.rb
user: login: rsamoilov
body: This method doesn't exist.
created_at: 2025-01-19T20:53:08Z
author_association: MEMBER
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
+
+  def remote_ip(*args)
+    @env['HTTP_X_FORWARDED_FOR']
+  end
path: lib/rage/request.rb
user: login: rsamoilov
body: I agree this is going to be more complicated. I would encourage you to work on `remote_ip` in a separate PR, but that's up to you.

Essentially, we want the algorithm to be close to [what it is in Rails](https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html) with the ability to customize the list of trusted proxies.
created_at: 2025-01-19T21:04:39Z
author_association: MEMBER
diff_hunk: @@ -1,11 +1,60 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require 'forwardable'
 
 class Rage::Request
+  extend Forwardable
+  attr_accessor :rack_request
+  # get?/post?/patch?/put?/delete?/head?
+  def_delegators :@rack_request,
+                 :ssl?,
+                 :host,
+                 :port,
+                 :query,
+                 :query_string,
+                 :env,
+                 :get_header,
+                 :get?,
+                 :post?,
+                 :patch?,
+                 :put?,
+                 :delete?,
+                 :head?
+
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
+
+  HTTP_METHOD_LOOKUP = {}
+
+  # Populate the HTTP method lookup cache.
+  # TODO: Extract underscore method
+  # HTTP_METHODS.each { |method|
+  #   HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
+  # }
+
   # @private
   def initialize(env)
     @env = env
+    @rack_request = Rack::Request.new(env)
path: lib/rage/request.rb
user: login: aaoafk
body: So I think the approach is similar in both regards. 

I think I was just following what is in rails but I added the extra explicit delegation via forwardable. Something I realized is that ActionDispatch request actually relies on the `@env` being initialized in the `Env` module and if we wanted access to any of the other methods defined in `Rack::Request::Helpers` and `Rack::Request::Env` then we should include them and call `super` first inside `Rage#Request`.

I tested the behavior by removing the delegated methods to `@rack_request`, adding the `super` call in `Rage::Request.initialize` and it is the same.

We really could do both, I think the difference with having `@rack_request` would just be that additional memory overhead from storing that object internally inside `Rage::Request`?
created_at: 2025-01-21T15:10:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
path: lib/rage/request.rb
user: login: aaoafk
body: I was handling the `method` method defined here: https://guides.rubyonrails.org/action_controller_overview.html#the-request-object

I implemented the `check_method` as just a linear scan of the `HTTP_METHODS` array:

```
  def check_method(name)
    http_methods_array = HTTP_METHODS
    if name
      if http_methods_array.include?(name)
        name
      else
        raise(Rage::Errors::UnknownHTTPMethod, "#{name}, accepted HTTP methods are #{http_methods_array}")
      end
    end
  end
```
I think we want something like this to avoid someone adding arbitrary headers.
created_at: 2025-01-21T15:13:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,60 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require 'forwardable'
 
 class Rage::Request
+  extend Forwardable
+  attr_accessor :rack_request
+  # get?/post?/patch?/put?/delete?/head?
+  def_delegators :@rack_request,
+                 :ssl?,
+                 :host,
+                 :port,
+                 :query,
path: lib/rage/request.rb
user: login: aaoafk
body: I removed `:query` dunno where I got that from lol
created_at: 2025-01-21T15:14:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
+
+  def remote_ip(*args)
+    @env['HTTP_X_FORWARDED_FOR']
+  end
path: lib/rage/request.rb
user: login: aaoafk
body: I realized that rack already includes behavior for this:

```
  def_delegator :@rack_request, :ip, :remote_ip
```

It's in `ip` method. Here I aliased it as `remote_ip` but that seems to be what rails is relying on for that behavior.

In terms of customizing the list of trusted proxies, I think we could just hook into rack behavior:

```
    trusted_proxies = Regexp.union(
      /\A127#{valid_ipv4_octet}{3}\z/,                          # localhost IPv4 range 127.x.x.x, per RFC-3330
      /\A::1\z/,                                                # localhost IPv6 ::1
      /\Af[cd][0-9a-f]{2}(?::[0-9a-f]{0,4}){0,7}\z/i,           # private IPv6 range fc00 .. fdff
      /\A10#{valid_ipv4_octet}{3}\z/,                           # private IPv4 range 10.x.x.x
      /\A172\.(1[6-9]|2[0-9]|3[01])#{valid_ipv4_octet}{2}\z/,   # private IPv4 range 172.16.0.0 .. 172.31.255.255
      /\A192\.168#{valid_ipv4_octet}{2}\z/,                     # private IPv4 range 192.168.x.x
      /\Alocalhost\z|\Aunix(\z|:)/i,                            # localhost hostname, and unix domain sockets
    )

    self.ip_filter = lambda { |ip| trusted_proxies.match?(ip) }
```

I'm thinking either add another `Regexp.union` with rack and a list that we pass down or modify `self.ip_filter`?
created_at: 2025-01-21T15:16:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,60 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require 'forwardable'
 
 class Rage::Request
+  extend Forwardable
+  attr_accessor :rack_request
+  # get?/post?/patch?/put?/delete?/head?
+  def_delegators :@rack_request,
+                 :ssl?,
+                 :host,
+                 :port,
+                 :query,
+                 :query_string,
+                 :env,
+                 :get_header,
+                 :get?,
+                 :post?,
+                 :patch?,
+                 :put?,
+                 :delete?,
+                 :head?
+
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
+
+  HTTP_METHOD_LOOKUP = {}
+
+  # Populate the HTTP method lookup cache.
+  # TODO: Extract underscore method
+  # HTTP_METHODS.each { |method|
+  #   HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
+  # }
+
   # @private
   def initialize(env)
     @env = env
+    @rack_request = Rack::Request.new(env)
path: lib/rage/request.rb
user: login: rsamoilov
body: I think I like your approach a bit more because it allows us to add to the class only the methods we are interested in.
created_at: 2025-01-22T20:14:10Z
author_association: MEMBER
diff_hunk: @@ -1,11 +1,60 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require 'forwardable'
 
 class Rage::Request
+  extend Forwardable
+  attr_accessor :rack_request
+  # get?/post?/patch?/put?/delete?/head?
+  def_delegators :@rack_request,
+                 :ssl?,
+                 :host,
+                 :port,
+                 :query,
+                 :query_string,
+                 :env,
+                 :get_header,
+                 :get?,
+                 :post?,
+                 :patch?,
+                 :put?,
+                 :delete?,
+                 :head?
+
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
+
+  HTTP_METHOD_LOOKUP = {}
+
+  # Populate the HTTP method lookup cache.
+  # TODO: Extract underscore method
+  # HTTP_METHODS.each { |method|
+  #   HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
+  # }
+
   # @private
   def initialize(env)
     @env = env
+    @rack_request = Rack::Request.new(env)
path: lib/rage/request.rb
user: login: rsamoilov
body: Is it possible to create `@rack_request` lazily?
created_at: 2025-01-22T20:14:39Z
author_association: MEMBER
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
path: lib/rage/request.rb
user: login: rsamoilov
body: Got it, makes sense. Should `HTTP_METHODS` be a `Set` then for faster lookups?
created_at: 2025-01-22T20:15:37Z
author_association: MEMBER
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
+
+  def remote_ip(*args)
+    @env['HTTP_X_FORWARDED_FOR']
+  end
path: lib/rage/request.rb
user: login: rsamoilov
body: Both options work. I guess modifying `self.ip_filter` would be somewhat more expected.
created_at: 2025-01-22T20:17:44Z
author_association: MEMBER
diff_hunk: @@ -1,11 +1,60 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require 'forwardable'
 
 class Rage::Request
+  extend Forwardable
+  attr_accessor :rack_request
+  # get?/post?/patch?/put?/delete?/head?
+  def_delegators :@rack_request,
+                 :ssl?,
+                 :host,
+                 :port,
+                 :query,
+                 :query_string,
+                 :env,
+                 :get_header,
+                 :get?,
+                 :post?,
+                 :patch?,
+                 :put?,
+                 :delete?,
+                 :head?
+
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
+
+  HTTP_METHOD_LOOKUP = {}
+
+  # Populate the HTTP method lookup cache.
+  # TODO: Extract underscore method
+  # HTTP_METHODS.each { |method|
+  #   HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
+  # }
+
   # @private
   def initialize(env)
     @env = env
+    @rack_request = Rack::Request.new(env)
path: lib/rage/request.rb
user: login: aaoafk
body: I don't think so. I tried it out and it looks like there might be some weird behavior with lazy loading and def_delegator, it was `nil` for all my specs, even with a:

```
def rack_request
  @rack_request ||= Rack::Request.new(env)
end
```
created_at: 2025-01-23T01:46:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
path: lib/rage/request.rb
user: login: aaoafk
body: I changed this to a set. I benchmarked it and it looks like it was 2x as fast for lookup compared to Array#include?
created_at: 2025-01-23T01:47:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
+
+  def remote_ip(*args)
+    @env['HTTP_X_FORWARDED_FOR']
+  end
path: lib/rage/request.rb
user: login: aaoafk
body: I modified the `initialize` method for `request` is that what you had in mind for custom proxy support?
created_at: 2025-01-23T01:47:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +117,50 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  def method(*args)
+    if args.empty?
+      @method ||= check_method(
+        get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
path: lib/rage/request.rb
user: login: rsamoilov
body: Why do we need to support `rack.methodoverride.original_method` here? I am trying to send a request to a Rage app with the `X-HTTP-Method-Override` header, but `rack.methodoverride.original_method` is still not getting set.
created_at: 2025-01-26T17:14:06Z
author_association: MEMBER
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: rsamoilov
body: So, if you include these two modules, does it mean we don't need the `def_delegators` calls and the `@rack_request` variable anymore?

In any case, we should explicitly document the new methods, as YARD doesn't pick up neither the `def_delegators` calls nor the methods from the included modules (you can verify by running `yard server --reload` and then visiting `http://localhost:8808/docs/Rage/Request`)
created_at: 2025-01-26T17:24:52Z
author_association: MEMBER
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
+
+  def remote_ip(*args)
+    @env['HTTP_X_FORWARDED_FOR']
+  end
path: lib/rage/request.rb
user: login: rsamoilov
body: Pretty much, but it should be part of the [configuration](https://github.com/rage-rb/rage/blob/master/lib/rage/configuration.rb) instead, same way it is in Rails - https://guides.rubyonrails.org/configuring.html#actiondispatch-remoteip.
created_at: 2025-01-26T17:26:58Z
author_association: MEMBER
diff_hunk: @@ -76,8 +125,54 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  # TODO: decide whether to rip `check_method` from rails
+  # it checks the http method against a list of all known
+  # http methods to ensure that it is a valid header?
+  def method(*args)
+    if args.empty?
+      # @method ||= check_method(
+      #   get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+      # )
+      @method ||= get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
+    else
+      super
+    end
+  end
+
+  def remote_ip(*args)
+    @env['HTTP_X_FORWARDED_FOR']
+  end
path: lib/rage/request.rb
user: login: aaoafk
body: I'll look into what I imagine to be that little configuration object but if memory serves correct I'm not sure that this module is being used anymore ill take another look, because that module resolves the remote ip but ActionDispatch request is forwarding it to rack
created_at: 2025-01-26T17:45:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: aaoafk
body: Virtually yes, we wouldn't need it. I think I would need to bring some methods down in that case if we got rid of `@rack_request` tho, would need to double check
created_at: 2025-01-26T17:52:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +117,50 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  def method(*args)
+    if args.empty?
+      @method ||= check_method(
+        get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
path: lib/rage/request.rb
user: login: aaoafk
body: I'll need to look into `rack.methodoverride.original_method` a little more closely... I'm guessing that header you set is supposed to lead to a different result, pretty much just ripped that right from rails didn't think too much about it.
created_at: 2025-01-26T17:54:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: aaoafk
body: So to close this out we have two options:

1) `@rack_request` with `def_delegator`.

2) include Rack::Request::Helpers and include Rack::Request::Env approach. This also works the same way, the same methods are covered but we will need to bring some methods down to comply with rails api. Look at this test output with composing the modules behavior:

```
Rage::Request
  returns the full URL
  returns the path
  returns the full path with query string
  returns the user agent
  handles missing user agent header
  handles the host property of a request
  handles the domain property of a request
  handles the method property of a request
  handles `get?` HTTP verb of a request
  handles `post?` HTTP verb of a request
  handles the `patch?` HTTP verb of a request
  handles the `put?` HTTP verb of a request
  handles the `delete?` HTTP verb of a request
  handles the `head?` HTTP verb of a request
  handles the port property of a request
  handles the protocol property of a request
  handles the query string property of a request
  handles the remote ip property of a request (FAILED - 1)
  handles the env property of a request
  handles the format property of a request (FAILED - 2)
```
Remote ip & format need to be brought down.

Having both `@rack_request` and the inclusion of these modules doesn't really make sense to me because it seems to be effectively duplicating behavior.

And just to round out the understanding here is really based around how the `@env` instance variable is set. `Rack::Request` sets the `@env` instance variable and includes `Rack::Request::Env` and `Rack::Request::Helpers` but `Rack::Request` has its own `initialize` method that sets `@env`.

 Assume you don't want to use `Rack::Request` then there is an `initialize` method in `Env` module that sets the `@env` instance variable that we can hook into with our `Request` class via `super`, which will then allow us to use the methods in both those modules without issue.
created_at: 2025-02-11T01:52:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: aaoafk
body: > So, if you include these two modules, does it mean we don't need the `def_delegators` calls and the `@rack_request` variable anymore?
> 
> In any case, we should explicitly document the new methods, as YARD doesn't pick up neither the `def_delegators` calls nor the methods from the included modules (you can verify by running `yard server --reload` and then visiting `http://localhost:8808/docs/Rage/Request`)

Based on the approach we take I will make sure that the documentation is `YARD` compatible.
created_at: 2025-02-11T02:03:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: rsamoilov
body: What about using `@rack_request` **without** `def_delegators`?


It would be a bit more verbose, but on the other hand, we’d have all methods we are interested in defined explicitly, which means less hassle with YARD. And we would also be able to create the `@rack_request` object lazily. 
created_at: 2025-02-11T23:52:50Z
author_association: MEMBER
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: aaoafk
body: Do you mean something like:

```
def host
  @rack_request.host
end
```

If so we would at least need to change the `env` inside `Rage::Request` to be an instance variable after which we can do: 

```
def rack_request
  @rack_request ||= Rack::Request.new(@env)
end
```

Since it needs `env`
created_at: 2025-02-11T23:57:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: rsamoilov
body: Yeah, that's what I meant. WDYT?
created_at: 2025-02-12T17:11:16Z
author_association: MEMBER
diff_hunk: @@ -2,17 +2,17 @@
 
 require "time"
 require "rack/request"
-require 'forwardable'
+require "forwardable"
 
 class Rage::Request
   extend Forwardable
-  attr_accessor :rack_request
-  # get?/post?/patch?/put?/delete?/head?
+  include Rack::Request::Helpers
+  include Rack::Request::Env
path: lib/rage/request.rb
user: login: aaoafk
body: Just did the refactor and that works fine. Let's go with this then.
created_at: 2025-02-12T23:23:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +117,50 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  def method(*args)
+    if args.empty?
+      @method ||= check_method(
+        get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
path: lib/rage/request.rb
user: login: aaoafk
body: So I was able to get this to work if I use the `Rack::MethodOverride` module inside a `Rage.configure` block:

```
Rage.configure do
  # Specify the number of server processes to run. Defaults to number of CPU cores.
  config.server.workers_count = 1

  # Specify the port the server will listen on.
  config.server.port = 3000

  # Specify the logger
  config.logger = Rage::Logger.new(STDOUT)

  config.middleware.use Rage::Reloader
  config.middleware.use Rack::MethodOverride
end
```
created_at: 2025-02-13T00:14:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,122 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require "forwardable"
 
 class Rage::Request
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS_SET = (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).to_set
+
+  attr_accessor :env
   # @private
-  def initialize(env)
+  def initialize(env, custom_proxies: nil)
     @env = env
+    # super(env)
+    after_initialize(custom_proxies) if custom_proxies
+  end
+
+  def after_initialize(custom_proxies)
+    if custom_proxies&.is_a?(Regexp)
+      Rack::Request.class_exec do |rage_trusted_proxies|
+        # hook on trusted_proxy? 
+      end
+    else
+      raise(Rage::Errors::InvalidCustomProxy, "Custom proxy should be a regexp. You passed in a #{custom_proxies.class}")
+    end
+  end
+
+  def rack_request
+    @rack_request ||= Rack::Request.new(@env)
+  end
+
+  def ssl?
+    rack_request.ssl?
+  end
+
+  def host
+    rack_request.host
+  end
+
+  def port
+    rack_request.port
+  end
+
+  def query_string
+    rack_request.query_string
+  end
+
+  def env
+    rack_request.env
path: lib/rage/request.rb
user: login: rsamoilov
body: ```suggestion
    @env
```
created_at: 2025-02-13T18:37:13Z
author_association: MEMBER
diff_hunk: @@ -1,11 +1,122 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require "forwardable"
 
 class Rage::Request
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS_SET = (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).to_set
+
+  attr_accessor :env
   # @private
-  def initialize(env)
+  def initialize(env, custom_proxies: nil)
     @env = env
+    # super(env)
+    after_initialize(custom_proxies) if custom_proxies
+  end
+
+  def after_initialize(custom_proxies)
+    if custom_proxies&.is_a?(Regexp)
+      Rack::Request.class_exec do |rage_trusted_proxies|
+        # hook on trusted_proxy? 
+      end
+    else
+      raise(Rage::Errors::InvalidCustomProxy, "Custom proxy should be a regexp. You passed in a #{custom_proxies.class}")
+    end
+  end
+
+  def rack_request
+    @rack_request ||= Rack::Request.new(@env)
+  end
path: lib/rage/request.rb
user: login: rsamoilov
body: Could we make this method private?
created_at: 2025-02-13T18:37:39Z
author_association: MEMBER
diff_hunk: @@ -1,11 +1,122 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require "forwardable"
path: lib/rage/request.rb
user: login: rsamoilov
body: ```suggestion
```
created_at: 2025-02-13T18:38:08Z
author_association: MEMBER
diff_hunk: @@ -1,11 +1,122 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
path: lib/rage/request.rb
user: login: rsamoilov
body: ```suggestion
```

Do we need to manually require `rack/request`?
created_at: 2025-02-13T18:38:50Z
author_association: MEMBER
diff_hunk: @@ -76,8 +117,50 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  def method(*args)
+    if args.empty?
+      @method ||= check_method(
+        get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
path: lib/rage/request.rb
user: login: rsamoilov
body: I still don't think we need it. What's the intended functionality here?
created_at: 2025-02-13T18:40:01Z
author_association: MEMBER
diff_hunk: @@ -121,7 +258,7 @@ def [](requested_header)
         if "CONTENT_TYPE" == requested_header || "CONTENT_LENGTH" == requested_header
           @env[requested_header]
         else
-          @env["#{HTTP}#{requested_header}"]
+          @env["#{HTTP_}#{requested_header}"]
path: lib/rage/request.rb
user: login: rsamoilov
body: ```suggestion
          @env["#{HTTP}#{requested_header}"]
```
created_at: 2025-02-13T18:40:28Z
author_association: MEMBER
diff_hunk: @@ -76,8 +117,50 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  def method(*args)
+    if args.empty?
+      @method ||= check_method(
+        get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
path: lib/rage/request.rb
user: login: aaoafk
body: AFAIK it is to get the correct HTTP method if its overwritten: `curl -v -X POST http://localhost:3000/ -H "X-HTTP-Method-Override: GET"`

If `X-HTTP-Method-Override` is set then get the HTTP verb that it sets there. Otherwise, grab the request method which is `POST`.

If a client wants to use this behavior then they would have to use `Rack::MethodOverride` middleware. Otherwise, we always get the request method, even if its overwritten by that header... Does that make sense?

We wouldn't actually include that middleware in the gem but I added it to show what the intended usage would be.
created_at: 2025-02-13T22:14:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,122 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
path: lib/rage/request.rb
user: login: aaoafk
body: You're right we don't need that require rack is already a dependency its redundant.
created_at: 2025-02-13T22:19:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,122 @@
 # frozen_string_literal: true
 
 require "time"
+require "rack/request"
+require "forwardable"
 
 class Rage::Request
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  HTTP_METHODS_SET = (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).to_set
+
+  attr_accessor :env
   # @private
-  def initialize(env)
+  def initialize(env, custom_proxies: nil)
     @env = env
+    # super(env)
+    after_initialize(custom_proxies) if custom_proxies
+  end
+
+  def after_initialize(custom_proxies)
+    if custom_proxies&.is_a?(Regexp)
+      Rack::Request.class_exec do |rage_trusted_proxies|
+        # hook on trusted_proxy? 
+      end
+    else
+      raise(Rage::Errors::InvalidCustomProxy, "Custom proxy should be a regexp. You passed in a #{custom_proxies.class}")
+    end
+  end
+
+  def rack_request
+    @rack_request ||= Rack::Request.new(@env)
+  end
path: lib/rage/request.rb
user: login: aaoafk
body: yes we can and probably should I moved it to private section
created_at: 2025-02-13T22:22:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,8 +117,50 @@ def user_agent
     @env["HTTP_USER_AGENT"]
   end
 
+  def domain(tld_length = 1)
+    extract_domain(host, tld_length)
+  end
+
+  def protocol
+    (ssl? == "https://") ? "https://" : "http://"
+  end
+
+  def method(*args)
+    if args.empty?
+      @method ||= check_method(
+        get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")
path: lib/rage/request.rb
user: login: rsamoilov
body: Aha, it makes sense now!
created_at: 2025-02-14T19:23:53Z
author_association: MEMBER
diff_hunk: @@ -3,11 +3,143 @@
 require "time"
 
 class Rage::Request
+  # regexp to match against a ip address
+  IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+  # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
+  RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+  # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
+  RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+  # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
+  RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+  # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
+  RFC3648 = %w(ORDERPATCH)
+  # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
+  RFC3744 = %w(ACL)
+  # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
+  RFC5323 = %w(SEARCH)
+  # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
+  RFC4791 = %w(MKCALENDAR)
+  # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
+  RFC5789 = %w(PATCH)
+
+  # Set data structure of all RFC defined HTTP headers
+  HTTP_METHODS_SET = (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).to_set
path: lib/rage/request.rb
user: login: rsamoilov
body: It looks like this line fails on Ruby 3.1. You can fix it by adding the following line to the top of the file:

```
require "set"
```
created_at: 2025-03-13T18:55:45Z
author_association: MEMBER
diff_hunk: @@ -63,7 +65,63 @@
     env.delete("HTTP_USER_AGENT")
     expect(request.user_agent).to be_nil
   end
+  
+  it "handles the host property of a request" do
+    expect(request.host).not_to be_nil
+  end
+
+  it "handles the domain property of a request" do
+    expect(request.domain).not_to be_nil
+  end
+
+  it "handles the method property of a request" do
+    expect(request.method).not_to be_nil
+  end
+
+  it "handles `get?` HTTP verb of a request" do
+    expect(request.get?).not_to be_nil 
+  end
+
+  it "handles `post?` HTTP verb of a request" do
+    expect(request.post?).not_to be_nil 
+  end
+
+  it "handles the `patch?` HTTP verb of a request" do
+    expect(request.patch?).not_to be_nil 
+  end
+
+  it "handles the `put?` HTTP verb of a request" do
+    expect(request.put?).not_to be_nil 
+  end
 
+  it "handles the `delete?` HTTP verb of a request" do
+    expect(request.delete?).not_to be_nil 
+  end
+
+  it "handles the `head?` HTTP verb of a request" do
+    expect(request.head?).not_to be_nil 
+  end
+
+  it "handles the port property of a request" do
+    expect(request.port).not_to be_nil
+  end
+
+  it "handles the protocol property of a request" do
+    expect(request.protocol).not_to be_nil
+  end
path: spec/rage/request_spec.rb
user: login: rsamoilov
body: Did you remove the `protocol` method from the `Request` class on purpose? This test is currently failing because there's no such method.
created_at: 2025-03-13T18:56:42Z
author_association: MEMBER
diff_hunk: @@ -63,7 +65,63 @@
     env.delete("HTTP_USER_AGENT")
     expect(request.user_agent).to be_nil
   end
+  
+  it "handles the host property of a request" do
+    expect(request.host).not_to be_nil
+  end
+
+  it "handles the domain property of a request" do
+    expect(request.domain).not_to be_nil
+  end
+
+  it "handles the method property of a request" do
+    expect(request.method).not_to be_nil
+  end
+
+  it "handles `get?` HTTP verb of a request" do
+    expect(request.get?).not_to be_nil 
+  end
+
+  it "handles `post?` HTTP verb of a request" do
+    expect(request.post?).not_to be_nil 
+  end
+
+  it "handles the `patch?` HTTP verb of a request" do
+    expect(request.patch?).not_to be_nil 
+  end
+
+  it "handles the `put?` HTTP verb of a request" do
+    expect(request.put?).not_to be_nil 
+  end
 
+  it "handles the `delete?` HTTP verb of a request" do
+    expect(request.delete?).not_to be_nil 
+  end
+
+  it "handles the `head?` HTTP verb of a request" do
+    expect(request.head?).not_to be_nil 
+  end
+
+  it "handles the port property of a request" do
+    expect(request.port).not_to be_nil
+  end
+
+  it "handles the protocol property of a request" do
+    expect(request.protocol).not_to be_nil
+  end
path: spec/rage/request_spec.rb
user: login: aaoafk
body: Strange. I added it back. I don't remember deleting it.
created_at: 2025-03-15T03:05:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: aaoafk
created_at: 2025-01-21T15:37:14Z
body: Also one thing I added to cover `format` as defined in rails api is just:

```
  def_delegator :@rack_request, :content_type, :format
```
author_association: CONTRIBUTOR
user: login: aaoafk
created_at: 2025-01-21T15:38:48Z
body: As an aside. I noticed that we have `url` implemented inside: `Rage::Request` like so:

```
  # Returns the full URL of the request.
  # @example
  #   request.url # => "https://example.com/users?show_archived=true"
  def url
    scheme = @env["rack.url_scheme"]
    host = @env["SERVER_NAME"]
    port = @env["SERVER_PORT"]
    path = @env["PATH_INFO"]
    query_string = @env["QUERY_STRING"]

    port_part = (scheme == "http" && port == "80") || (scheme == "https" && port == "443") ? "" : ":#{port}"
    query_part = query_string.empty? ? "" : "?#{query_string}"

    "#{scheme}://#{host}#{port_part}#{path}#{query_part}"
  end
```

Is there a reason we don't want to use `url` behavior defined inside rack?

```
      # Tries to return a remake of the original request URL as a string.
      def url
        base_url + fullpath
      end
```
author_association: CONTRIBUTOR
user: login: rsamoilov
created_at: 2025-01-22T20:18:41Z
body: > As an aside. I noticed that we have `url` implemented inside: `Rage::Request` like so:
> 
> ```
>   # Returns the full URL of the request.
>   # @example
>   #   request.url # => "https://example.com/users?show_archived=true"
>   def url
>     scheme = @env["rack.url_scheme"]
>     host = @env["SERVER_NAME"]
>     port = @env["SERVER_PORT"]
>     path = @env["PATH_INFO"]
>     query_string = @env["QUERY_STRING"]
> 
>     port_part = (scheme == "http" && port == "80") || (scheme == "https" && port == "443") ? "" : ":#{port}"
>     query_part = query_string.empty? ? "" : "?#{query_string}"
> 
>     "#{scheme}://#{host}#{port_part}#{path}#{query_part}"
>   end
> ```
> 
> Is there a reason we don't want to use `url` behavior defined inside rack?
> 
> ```
>       # Tries to return a remake of the original request URL as a string.
>       def url
>         base_url + fullpath
>       end
> ```

No real reason. Feel free to update it.
author_association: MEMBER
user: login: rsamoilov
created_at: 2025-01-26T17:29:07Z
body: Hey @aaoafk , really great job so far! Sorry I wasn't able to provide prompt feedback, but I promise I will get better 😄 
author_association: MEMBER
user: login: aaoafk
created_at: 2025-01-26T17:47:13Z
body: > Hey @aaoafk , really great job so far! Sorry I wasn't able to provide prompt feedback, but I promise I will get better 😄

that's fine im more so just trying to get better at programming so your reviews are helping no pressure im working on this in my free time lol
author_association: CONTRIBUTOR
user: login: rsamoilov
created_at: 2025-02-10T20:26:11Z
body: Hey @aaoafk , how's it going? Is there anything I can help you with?
author_association: MEMBER
user: login: aaoafk
created_at: 2025-02-11T01:23:04Z
body: Hey @rsamoilov recently had a big life change, moved to Puerto Rico for work so I was pretty busy the last couple weeks sorting everything out. I'll try to take a look at this soon.
author_association: CONTRIBUTOR
user: login: aaoafk
created_at: 2025-02-12T23:31:30Z
body: I committed changes for you to view I still need to look at `rack.methodoverride.original_method` and the `custom_proxies` stuff
author_association: CONTRIBUTOR
user: login: rsamoilov
created_at: 2025-02-13T18:42:16Z
body: > I committed changes for you to view I still need to look at `rack.methodoverride.original_method` and the `custom_proxies` stuff

It lacks YARD docs, but apart from that, this bit looks good 👍 
author_association: MEMBER
user: login: aaoafk
created_at: 2025-02-14T22:16:42Z
body: I'll look at adding YARD Docs and the custom proxies bit then
author_association: CONTRIBUTOR
user: login: aaoafk
created_at: 2025-02-17T17:23:55Z
body: @rsamoilov are you imagining that the `custom_proxies` bit is a part of `config.server.custom_proxies` or `config.custom_proxies` I think the former seems properly scoped
author_association: CONTRIBUTOR
user: login: rsamoilov
created_at: 2025-02-18T20:39:55Z
body: > @rsamoilov are you imagining that the `custom_proxies` bit is a part of `config.server.custom_proxies` or `config.custom_proxies` I think the former seems properly scoped

`config.server` is more about the server process - we can use it to set the port the server is running on or the number of workers. How about we add a new `config.http` scope and put `custom_proxies` (should actually be `trusted_proxies`) in there?
author_association: MEMBER
user: login: rsamoilov
created_at: 2025-03-11T20:04:06Z
body: Hey @aaoafk , are you still working on the PR?

If you are struggling with the `remote_ip` stuff, I would encourage you to move it to a different PR because everything else is pretty much ready to merge.
author_association: MEMBER
user: login: aaoafk
created_at: 2025-03-11T20:06:06Z
body: Yeah I was thinking it might take a little bit for that one I'll remove the remote ip stuff and do a push tonight 
author_association: CONTRIBUTOR
user: login: aaoafk
created_at: 2025-03-13T00:09:52Z
body: Hey Roman. I think this should have everything around the remote_ip removed and I corrected the merge conflict, I think we just wanted to take changes from both files since they weren't really conflicting.
author_association: CONTRIBUTOR
user: login: aaoafk
created_at: 2025-03-15T03:33:35Z
body: One test is failing now and I think it might be due to a stale value for the `SERVER_PORT` inside this test:

```
  it "handles default ports in URL" do
    env["SERVER_PORT"] = "80"
    expect(request.url).to eq("http://localhost/users?show_archived=true")

    env["rack.url_scheme"] = "https"
    env["SERVER_PORT"] = "443"
    expect(request.url).to eq("https://localhost/users?show_archived=true")
  end
```

The output is:

expected: "http://localhost/users?show_archived=true"
got: "http://localhost:3000/users?show_archived=true"

I think this is because of the memoized request, it is using an old value for env, since the test does a direct modification on env it wouldn't be reflected there, but some rack methods do query headers directly from env so...

As an aside ill probably re-write the tests to check for literal values instead of just checking not nil...
author_association: CONTRIBUTOR
user: login: rsamoilov
created_at: 2025-03-16T14:32:40Z
body: Aha, good catch! Those tests should use contexts instead. Do you want me to fix those?
author_association: MEMBER
user: login: aaoafk
created_at: 2025-03-16T14:48:06Z
body: Do you mean rspec contexts ? 
author_association: CONTRIBUTOR
user: login: rsamoilov
created_at: 2025-03-16T16:14:55Z
body: Yep. 
author_association: MEMBER
user: login: aaoafk
created_at: 2025-03-16T20:40:07Z
body: How does this look? BTW the previous issue was because of 'HTTP_HOST' header being set to 'localhost:3000'. It needs to be set each time the test is using that for context otherwise it pulls from SERVER_* headers 
author_association: CONTRIBUTOR
user: login: rsamoilov
created_at: 2025-03-17T17:00:15Z
body: Rubocop is failing, but it looks like it's picked up the wrong commit.
author_association: MEMBER
user: login: aaoafk
created_at: 2025-03-17T18:31:10Z
body: 🔥 I'll see about remote ip next or something else curious about the implementation 
author_association: CONTRIBUTOR