tina4ruby 3.12.6 → 3.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tina4/rack_app.rb +49 -4
- data/lib/tina4/router.rb +69 -0
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 351a496d9e7573b3c2f9c45bb170612d3c9f553f787b937d1a402b0f351cb427
|
|
4
|
+
data.tar.gz: bec0461f253af01dbf747d346bda6d705eada5538fd36adb7741ae2d6a0fafd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a86b778c89cffba395091c7f1d5e7c9748ca6b95387b0748f4bef5f47d0c4c606f932f535ff0a6b5eef7de8e74970ba25b6c2b53a5d3bc63df3c3f29efae5020
|
|
7
|
+
data.tar.gz: 755890be8270b90bcbbc20d5fae1e76aad5e507346c6490196077143576268723736ed5f88e7ab6e10fc1391f59806dc25bcb18b5ca47730151e76c3b1946bab
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -43,8 +43,18 @@ module Tina4
|
|
|
43
43
|
path = env["PATH_INFO"] || "/"
|
|
44
44
|
request_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
45
45
|
|
|
46
|
-
# Fast-path:
|
|
47
|
-
|
|
46
|
+
# Fast-path: CORS preflight. Real CORS preflight requests carry an
|
|
47
|
+
# Origin header AND an Access-Control-Request-Method header — the
|
|
48
|
+
# browser is asking "may I send this method?" before the actual
|
|
49
|
+
# request. If neither is present, the OPTIONS is a plain protocol-
|
|
50
|
+
# introspection request (link checker, monitoring probe, RFC 9110
|
|
51
|
+
# §9.3.7 OPTIONS) and must fall through to the router's generic
|
|
52
|
+
# Allow-header response. Otherwise we'd shadow the framework's own
|
|
53
|
+
# OPTIONS support and force every operator to hand-register CORS
|
|
54
|
+
# exceptions for every introspection client.
|
|
55
|
+
if method == "OPTIONS" && (env["HTTP_ORIGIN"] || env["HTTP_ACCESS_CONTROL_REQUEST_METHOD"])
|
|
56
|
+
return Tina4::CorsMiddleware.preflight_response(env)
|
|
57
|
+
end
|
|
48
58
|
|
|
49
59
|
# WebSocket upgrade — match against registered ws_routes
|
|
50
60
|
if websocket_upgrade?(env)
|
|
@@ -87,8 +97,43 @@ module Tina4
|
|
|
87
97
|
rack_response = handle_route(env, route, path_params)
|
|
88
98
|
matched_pattern = route.path
|
|
89
99
|
else
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
# RFC 9110 conformance — before falling through to 404, check whether
|
|
101
|
+
# the PATH is known to the router under any OTHER method.
|
|
102
|
+
# - OPTIONS request → 204 with Allow header (§9.3.7)
|
|
103
|
+
# - Any other method (PUT on GET-only, TRACE, CONNECT, etc.)
|
|
104
|
+
# → 405 with Allow header (§15.5.6 + §10.2.1)
|
|
105
|
+
allowed = Tina4::Router.methods_allowed_for_path(path)
|
|
106
|
+
if !allowed.empty?
|
|
107
|
+
allow_header = allowed.join(", ")
|
|
108
|
+
if method.to_s.upcase == "OPTIONS"
|
|
109
|
+
rack_response = [204, { "allow" => allow_header, "content-length" => "0" }, [""]]
|
|
110
|
+
else
|
|
111
|
+
body = %({"error":"Method Not Allowed","path":"#{path}","method":"#{method}","allow":[#{allowed.map { |m| %("#{m}") }.join(",")}],"status":405})
|
|
112
|
+
rack_response = [405, {
|
|
113
|
+
"allow" => allow_header,
|
|
114
|
+
"content-type" => "application/json",
|
|
115
|
+
"content-length" => body.bytesize.to_s
|
|
116
|
+
}, [body]]
|
|
117
|
+
end
|
|
118
|
+
matched_pattern = nil
|
|
119
|
+
else
|
|
120
|
+
rack_response = handle_404(path)
|
|
121
|
+
matched_pattern = nil
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# RFC 9110 §9.3.2: a HEAD response MUST NOT include content. Strip
|
|
126
|
+
# the body unconditionally and record what Content-Length the GET
|
|
127
|
+
# would have sent. Cache validators / link checkers / monitoring
|
|
128
|
+
# probes use that header to estimate sizes.
|
|
129
|
+
if method.to_s.upcase == "HEAD"
|
|
130
|
+
status, headers, body_parts = rack_response
|
|
131
|
+
joined = body_parts.respond_to?(:join) ? body_parts.join : body_parts.to_s
|
|
132
|
+
unless joined.empty?
|
|
133
|
+
new_headers = headers.dup
|
|
134
|
+
new_headers["content-length"] = joined.bytesize.to_s
|
|
135
|
+
rack_response = [status, new_headers, [""]]
|
|
136
|
+
end
|
|
92
137
|
end
|
|
93
138
|
|
|
94
139
|
# Capture request for dev inspector
|
data/lib/tina4/router.rb
CHANGED
|
@@ -312,6 +312,25 @@ module Tina4
|
|
|
312
312
|
add("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
313
313
|
end
|
|
314
314
|
|
|
315
|
+
# Register an explicit HEAD route. By default the framework auto-handles
|
|
316
|
+
# HEAD by falling back to the GET route and stripping the body
|
|
317
|
+
# (RFC 9110 §9.3.2). Use this only when you need a HEAD handler that
|
|
318
|
+
# does something different from GET — e.g. cheaper existence-check
|
|
319
|
+
# logic, custom validator headers without the cost of building the body.
|
|
320
|
+
# The framework still strips the response body for you on the way out.
|
|
321
|
+
def head(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
322
|
+
add("HEAD", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Register an explicit OPTIONS route. By default the framework auto-
|
|
326
|
+
# handles OPTIONS by building an Allow header from every method
|
|
327
|
+
# registered for the path and returning 204 (RFC 9110 §9.3.7). Use
|
|
328
|
+
# this to take over that behaviour — e.g. to return a richer OPTIONS
|
|
329
|
+
# payload describing the resource.
|
|
330
|
+
def options(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
331
|
+
add("OPTIONS", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
332
|
+
end
|
|
333
|
+
|
|
315
334
|
def find_route(method, path)
|
|
316
335
|
normalized_method = method.upcase
|
|
317
336
|
# Normalize path once (not per-route)
|
|
@@ -325,9 +344,59 @@ module Tina4
|
|
|
325
344
|
params = route.match_path(normalized_path)
|
|
326
345
|
return [route, params] if params
|
|
327
346
|
end
|
|
347
|
+
|
|
348
|
+
# RFC 9110 §9.3.2: HEAD is identical to GET except for the absence
|
|
349
|
+
# of a response body. If no explicit HEAD route matched, fall back
|
|
350
|
+
# to the GET route — the dispatcher strips the body on the way out
|
|
351
|
+
# so the handler doesn't need to know HEAD even happened.
|
|
352
|
+
if normalized_method == "HEAD"
|
|
353
|
+
(method_index["GET"] || []).each do |route|
|
|
354
|
+
params = route.match_path(normalized_path)
|
|
355
|
+
return [route, params] if params
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
328
359
|
nil
|
|
329
360
|
end
|
|
330
361
|
|
|
362
|
+
# Return the list of HTTP methods registered for ``path``, in the order
|
|
363
|
+
# GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS. Used by the
|
|
364
|
+
# dispatcher to build the ``Allow:`` header on 405 / OPTIONS responses
|
|
365
|
+
# (RFC 9110 §10.2.1, §9.3.7).
|
|
366
|
+
#
|
|
367
|
+
# If GET is registered for the path, HEAD is appended implicitly
|
|
368
|
+
# (HEAD auto-fallback). OPTIONS is appended whenever the path has any
|
|
369
|
+
# registered method (the framework auto-handles OPTIONS).
|
|
370
|
+
def methods_allowed_for_path(path)
|
|
371
|
+
normalized_path = path.gsub("\\", "/")
|
|
372
|
+
normalized_path = "/#{normalized_path}" unless normalized_path.start_with?("/")
|
|
373
|
+
normalized_path = normalized_path.chomp("/") unless normalized_path == "/"
|
|
374
|
+
|
|
375
|
+
method_order = %w[GET POST PUT PATCH DELETE HEAD OPTIONS]
|
|
376
|
+
seen = []
|
|
377
|
+
any_matched = false
|
|
378
|
+
|
|
379
|
+
method_index.each do |m, routes_for_method|
|
|
380
|
+
next if routes_for_method.empty?
|
|
381
|
+
matched = routes_for_method.any? { |r| r.match_path(normalized_path) }
|
|
382
|
+
next unless matched
|
|
383
|
+
if m == "ANY"
|
|
384
|
+
any_matched = true
|
|
385
|
+
elsif method_order.include?(m)
|
|
386
|
+
seen << m unless seen.include?(m)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
seen = method_order.dup if any_matched
|
|
391
|
+
|
|
392
|
+
if !seen.empty?
|
|
393
|
+
seen << "HEAD" if seen.include?("GET") && !seen.include?("HEAD")
|
|
394
|
+
seen << "OPTIONS" unless seen.include?("OPTIONS")
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
method_order.select { |m| seen.include?(m) }
|
|
398
|
+
end
|
|
399
|
+
|
|
331
400
|
# When TINA4_TRAILING_SLASH_REDIRECT is truthy, the rack app uses this
|
|
332
401
|
# to detect whether the *original* (un-stripped) path differed from the
|
|
333
402
|
# canonical form so it can issue a 301 redirect. Default false — silent
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.12.
|
|
4
|
+
version: 3.12.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|