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.



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

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

  # Raw socket peer — NEVER honours X-Forwarded-For, so it can be trusted
  # for security decisions. Resolved BEFORE @ip: the peer decides whether
  # the forwarding headers may be believed at all.
  @remote_ip = (env["REMOTE_ADDR"] || "").to_s
  @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.



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

def content_type
  @content_type
end

#envObject (readonly)

Returns the value of attribute env.



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

def env
  @env
end

#ipObject (readonly)

Returns the value of attribute ip.



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

def ip
  @ip
end

#methodObject (readonly)

Returns the value of attribute method.



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

def method
  @method
end

#pathObject (readonly)

Returns the value of attribute path.



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

def path
  @path
end

#path_paramsObject

Returns the value of attribute path_params.



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

def path_params
  @path_params
end

#query_stringObject (readonly)

Returns the value of attribute query_string.



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

def query_string
  @query_string
end

#remote_ipObject (readonly)

Returns the value of attribute remote_ip.



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

def remote_ip
  @remote_ip
end

#userObject

Returns the value of attribute user.



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

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.



177
178
179
180
181
182
183
184
185
186
# File 'lib/tina4/request.rb', line 177

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



278
279
280
# File 'lib/tina4/request.rb', line 278

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

#bearer_tokenObject



297
298
299
300
# File 'lib/tina4/request.rb', line 297

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.



229
230
231
# File 'lib/tina4/request.rb', line 229

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.)



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

def body_raw
  @body_raw ||= read_body
end

#cookiesObject



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

def cookies
  @cookies ||= parse_cookies
end

#filesObject



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

def files
  @files ||= extract_files
end

#header(name) ⇒ Object



282
283
284
285
286
287
# File 'lib/tina4/request.rb', line 282

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



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

def headers
  @headers ||= extract_headers
end

#json_bodyObject



289
290
291
292
293
294
295
# File 'lib/tina4/request.rb', line 289

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).



274
275
276
# File 'lib/tina4/request.rb', line 274

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

#paramsObject



269
270
271
# File 'lib/tina4/request.rb', line 269

def params
  @params ||= build_params
end

#queryObject

Parsed query string as hash



245
246
247
# File 'lib/tina4/request.rb', line 245

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

#sessionObject

The session for THIS request.

degrade_on_backend_failure: this is the live request path, so a storage handler that cannot be built (an unreachable database, a refused backend name) is LOGGED and then degraded to an in-memory-only session rather than unwinding into RackApp's 500 handler and taking the request down with it (ADR-0021). TINA4_SESSION_STRICT still re-raises. Direct Session.new callers keep the loud raise - see the guard in Session#initialize.



221
222
223
# File 'lib/tina4/request.rb', line 221

def session
  @session ||= Tina4::Session.new(@env, degrade_on_backend_failure: true)
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.



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/tina4/request.rb', line 191

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