Class: Tina4::Request

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/request.rb

Defined Under Namespace

Classes: PayloadTooLarge

Constant Summary collapse

TINA4_MAX_UPLOAD_SIZE =

Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var.

Integer(ENV.fetch("TINA4_MAX_UPLOAD_SIZE", 10_485_760))

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env, path_params = {}) ⇒ Request

Returns a new instance of Request.



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
# File 'lib/tina4/request.rb', line 125

def initialize(env, path_params = {})
  @env = env
  @method = env["REQUEST_METHOD"]
  @path = env["PATH_INFO"] || "/"
  @query_string = env["QUERY_STRING"] || ""
  @content_type = env["CONTENT_TYPE"] || ""
  @path_params = path_params

  # Check upload size limit
  content_length = (env["CONTENT_LENGTH"] || 0).to_i
  if content_length > TINA4_MAX_UPLOAD_SIZE
    raise PayloadTooLarge,
      "Request body (#{content_length} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (#{TINA4_MAX_UPLOAD_SIZE} bytes)"
  end

  # Client IP with X-Forwarded-For support
  @ip = extract_client_ip

  # Lazy-initialized fields (nil = not yet computed)
  @headers = nil
  @cookies = nil
  @session = nil
  @body_raw = nil
  @params = nil
  @files = nil
  @json_body = nil
  @query_hash = nil
  @body_parsed = nil
end

Instance Attribute Details

#content_typeObject (readonly)

Returns the value of attribute content_type.



116
117
118
# File 'lib/tina4/request.rb', line 116

def content_type
  @content_type
end

#envObject (readonly)

Returns the value of attribute env.



116
117
118
# File 'lib/tina4/request.rb', line 116

def env
  @env
end

#ipObject (readonly)

Returns the value of attribute ip.



116
117
118
# File 'lib/tina4/request.rb', line 116

def ip
  @ip
end

#methodObject (readonly)

Returns the value of attribute method.



116
117
118
# File 'lib/tina4/request.rb', line 116

def method
  @method
end

#pathObject (readonly)

Returns the value of attribute path.



116
117
118
# File 'lib/tina4/request.rb', line 116

def path
  @path
end

#path_paramsObject (readonly)

Returns the value of attribute path_params.



116
117
118
# File 'lib/tina4/request.rb', line 116

def path_params
  @path_params
end

#query_stringObject (readonly)

Returns the value of attribute query_string.



116
117
118
# File 'lib/tina4/request.rb', line 116

def query_string
  @query_string
end

#userObject

Returns the value of attribute user.



118
119
120
# File 'lib/tina4/request.rb', line 118

def user
  @user
end

Class Method Details

.secure_scheme?(env) ⇒ Boolean

Is this request HTTPS from the CLIENT's point of view?

TLS is normally terminated at a proxy (nginx, HAProxy, ALB, Cloudflare, most container deploys) that then forwards plain HTTP to the app, so rack.url_scheme is "http" on exactly the deployments that ARE encrypted. x-forwarded-proto carries the scheme the client actually used, and a chain of proxies appends each hop ("https, http") — the FIRST is the client-facing one. Falls back to the native rack scheme, then the CGI HTTPS var.

This is the single source of truth for BOTH the URL scheme (#url) and the session-cookie Secure flag (Session#cookie_header). They used to decide it independently and could disagree — url() said https while the cookie concluded plain HTTP and dropped Secure (ruby#31, parity with PHP Request::isSecureScheme / tina4-php#175).

Parameters:

  • env (Hash)

    the Rack environment.

Returns:

  • (Boolean)

    true when the client's scheme is https.



173
174
175
176
177
178
179
180
181
182
# File 'lib/tina4/request.rb', line 173

def self.secure_scheme?(env)
  forwarded = (env["HTTP_X_FORWARDED_PROTO"] || "").to_s
  unless forwarded.strip.empty?
    return forwarded.split(",").first.to_s.strip.casecmp("https").zero?
  end
  scheme = (env["rack.url_scheme"] || "").to_s
  return true if scheme.casecmp("https").zero?
  https = (env["HTTPS"] || "").to_s
  !https.empty? && https.casecmp("off") != 0
end

Instance Method Details

#[](key) ⇒ Object



252
253
254
# File 'lib/tina4/request.rb', line 252

def [](key)
  params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
end

#bearer_tokenObject



271
272
273
274
# File 'lib/tina4/request.rb', line 271

def bearer_token
  auth = header("authorization") || ""
  auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
end

#bodyObject Also known as: body_parsed

Parsed body (JSON -> Hash, form-urlencoded -> Hash, multipart -> fields Hash, else the current fallback). This matches Python's request.body, PHP's, and Node's: body is the PARSED payload, not the raw bytes. For the raw string use body_raw.



217
218
219
# File 'lib/tina4/request.rb', line 217

def body
  @body_parsed ||= parse_body
end

#body_rawObject

Raw body string — the bytes exactly as the client sent them. (This is what body used to return before the cross-framework parity flip; SOAP/GraphQL and any consumer that needs the raw text reads this.)



225
226
227
# File 'lib/tina4/request.rb', line 225

def body_raw
  @body_raw ||= read_body
end

#cookiesObject



205
206
207
# File 'lib/tina4/request.rb', line 205

def cookies
  @cookies ||= parse_cookies
end

#filesObject



237
238
239
# File 'lib/tina4/request.rb', line 237

def files
  @files ||= extract_files
end

#header(name) ⇒ Object



256
257
258
259
260
261
# File 'lib/tina4/request.rb', line 256

def header(name)
  # Headers are stored in a CaseInsensitiveHash keyed by lowercase-
  # dashed names ("content-type", "x-api-key"). The hash normalises
  # the lookup case automatically, so pass the dashed form through.
  headers[name.to_s.tr("_", "-")]
end

#headersObject

Lazy accessors



201
202
203
# File 'lib/tina4/request.rb', line 201

def headers
  @headers ||= extract_headers
end

#json_bodyObject



263
264
265
266
267
268
269
# File 'lib/tina4/request.rb', line 263

def json_body
  @json_body ||= begin
    JSON.parse(body_raw)
  rescue JSON::ParserError, TypeError
    {}
  end
end

#param(key, default = nil) ⇒ Object

Look up a param by symbol or string key (indifferent access shortcut).



248
249
250
# File 'lib/tina4/request.rb', line 248

def param(key, default = nil)
  params[key.to_s] || params[key.to_sym] || default
end

#paramsObject

Merged params: query + body + path_params (path_params highest priority) Supports both string and symbol key access (indifferent access).



243
244
245
# File 'lib/tina4/request.rb', line 243

def params
  @params ||= build_params
end

#queryObject

Parsed query string as hash



233
234
235
# File 'lib/tina4/request.rb', line 233

def query
  @query_hash ||= parse_query_to_hash(@query_string)
end

#sessionObject



209
210
211
# File 'lib/tina4/request.rb', line 209

def session
  @session ||= Tina4::Session.new(@env)
end

#urlObject

Full absolute URL — scheme://host/path. Honours X-Forwarded-Proto / X-Forwarded-Host so apps behind a proxy still see the URL the client used. Matches Python/PHP/Node parity.



187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/tina4/request.rb', line 187

def url
  scheme = self.class.secure_scheme?(env) ? "https" : "http"
  host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
  port = env["SERVER_PORT"]
  url_str = "#{scheme}://#{host}"
  # Only append :port when the host doesn't already include one
  # (HTTP_HOST often does) and it's not the default for the scheme.
  url_str += ":#{port}" if port && !host.include?(":") && port.to_s != "80" && port.to_s != "443"
  url_str += @path
  url_str += "?#{@query_string}" unless @query_string.empty?
  url_str
end