1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
|
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2019-2025, by Samuel Williams.
require_relative "body/buffered"
require_relative "body/reader"
require_relative "headers"
require_relative "methods"
module Protocol
module HTTP
# Represents an HTTP request which can be used both server and client-side.
#
# ~~~ ruby
# require 'protocol/http'
#
# # Long form:
# Protocol::HTTP::Request.new("http", "example.com", "GET", "/index.html", "HTTP/1.1", Protocol::HTTP::Headers[["accept", "text/html"]])
#
# # Short form:
# Protocol::HTTP::Request["GET", "/index.html", {"accept" => "text/html"}]
# ~~~
class Request
prepend Body::Reader
# Initialize the request.
#
# @parameter scheme [String | Nil] The request scheme, usually `"http"` or `"https"`.
# @parameter authority [String | Nil] The request authority, usually a hostname and port number, e.g. `"example.com:80"`.
# @parameter method [String | Nil] The request method, usually one of `"GET"`, `"HEAD"`, `"POST"`, `"PUT"`, `"DELETE"`, `"CONNECT"` or `"OPTIONS"`, etc.
# @parameter path [String | Nil] The request path, usually a path and query string, e.g. `"/index.html"`, `"/search?q=hello"`, etc.
# @parameter version [String | Nil] The request version, usually `"http/1.0"`, `"http/1.1"`, `"h2"`, or `"h3"`.
# @parameter headers [Headers] The request headers, usually containing metadata associated with the request such as the `"user-agent"`, `"accept"` (content type), `"accept-language"`, etc.
# @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)
@scheme = scheme
@authority = authority
@method = method
@path = path
@version = version
@headers = headers
@body = body
@protocol = protocol
@interim_response = interim_response
end
# @attribute [String] the request scheme, usually `"http"` or `"https"`.
attr_accessor :scheme
# @attribute [String] the request authority, usually a hostname and port number, e.g. `"example.com:80"`.
attr_accessor :authority
# @attribute [String] the request method, usually one of `"GET"`, `"HEAD"`, `"POST"`, `"PUT"`, `"DELETE"`, `"CONNECT"` or `"OPTIONS"`, etc.
attr_accessor :method
# @attribute [String] the request path, usually a path and query string, e.g. `"/index.html"`, `"/search?q=hello"`, however it can be any [valid request target](https://www.rfc-editor.org/rfc/rfc9110#target.resource).
attr_accessor :path
# @attribute [String] the request version, usually `"http/1.0"`, `"http/1.1"`, `"h2"`, or `"h3"`.
attr_accessor :version
# @attribute [Headers] the request headers, usually containing metadata associated with the request such as the `"user-agent"`, `"accept"` (content type), `"accept-language"`, etc.
attr_accessor :headers
# @attribute [Body::Readable] the request body. It should only be read once (it may not be idempotent).
attr_accessor :body
# @attribute [String | Array(String) | Nil] the request protocol, usually empty, but occasionally `"websocket"` or `"webtransport"`. In HTTP/1, it is used to request a connection upgrade, and in HTTP/2 it is used to indicate a specfic protocol for the stream.
attr_accessor :protocol
# @attribute [Proc] a callback which is called when an interim response is received.
attr_accessor :interim_response
# 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.
def peer
nil
end
# Send the request to the given connection.
def call(connection)
connection.call(self)
end
# Send an interim response back to the origin of this request, if possible.
def send_interim_response(status, headers)
@interim_response&.call(status, headers)
end
# Register a callback to be called when an interim response is received.
#
# @yields {|status, headers| ...} The callback to be called when an interim response is received.
# @parameter status [Integer] The HTTP status code, e.g. `100`, `101`, etc.
# @parameter headers [Hash] The headers, e.g. `{"link" => "</style.css>; rel=stylesheet"}`, etc.
def on_interim_response(&block)
if interim_response = @interim_response
@interim_response = ->(status, headers) do
block.call(status, headers)
interim_response.call(status, headers)
end
else
@interim_response = block
end
end
# Whether this is a HEAD request: no body is expected in the response.
def head?
@method == Methods::HEAD
end
# Whether this is a CONNECT request: typically used to establish a tunnel.
def connect?
@method == Methods::CONNECT
end
# A short-cut method which exposes the main request variables that you'd typically care about.
#
# @parameter method [String] The HTTP method, e.g. `"GET"`, `"POST"`, etc.
# @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)
path = path&.to_s
body = Body::Buffered.wrap(body)
headers = Headers[headers]
self.new(scheme, authority, method, path, nil, headers, body, protocol, interim_response)
end
# Whether the request can be replayed without side-effects.
def idempotent?
@method != Methods::POST && (@body.nil? || @body.empty?)
end
# Convert the request to a hash, suitable for serialization.
#
# @returns [Hash] The request as a hash.
def as_json(...)
{
scheme: @scheme,
authority: @authority,
method: @method,
path: @path,
version: @version,
headers: @headers&.as_json,
body: @body&.as_json,
protocol: @protocol
}
end
# Convert the request to JSON.
#
# @returns [String] The request as JSON.
def to_json(...)
as_json.to_json(...)
end
# Summarize the request as a string.
#
# @returns [String] The request as a string.
def to_s
"#{@scheme}://#{@authority}: #{@method} #{@path} #{@version}"
end
end
end
end
|