Class: Tina4::Middleware

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

Class Method Summary collapse

Class Method Details

.after(pattern = nil, &block) ⇒ Object



28
29
30
# File 'lib/tina4/middleware.rb', line 28

def after(pattern = nil, &block)
  after_handlers << { pattern: pattern, handler: block }
end

.after_handlersObject



10
11
12
# File 'lib/tina4/middleware.rb', line 10

def after_handlers
  @after_handlers ||= []
end

.before(pattern = nil, &block) ⇒ Object



24
25
26
# File 'lib/tina4/middleware.rb', line 24

def before(pattern = nil, &block)
  before_handlers << { pattern: pattern, handler: block }
end

.before_handlersObject



6
7
8
# File 'lib/tina4/middleware.rb', line 6

def before_handlers
  @before_handlers ||= []
end

.clear!Object



64
65
66
67
68
# File 'lib/tina4/middleware.rb', line 64

def clear!
  @before_handlers = []
  @after_handlers = []
  @global_middleware = []
end

.get_globalObject

Parity alias matching Python/PHP/Node orchestrators.



20
21
22
# File 'lib/tina4/middleware.rb', line 20

def get_global
  global_middleware.dup
end

.global_middlewareObject

Registry of class-based middleware (registered via Router.use)



15
16
17
# File 'lib/tina4/middleware.rb', line 15

def global_middleware
  @global_middleware ||= []
end

.middleware_500(response, label, error) ⇒ Object

Deterministic clean 500 for a middleware that threw. Logs the cause (NEVER silent) then sets the response to the canonical error shape — byte-identical to the Python master (Server Error", "status":500 + status 500). Returns the response for chaining.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/tina4/middleware.rb', line 198

def middleware_500(response, label, error)
  begin
    Tina4::Log.error(
      "Middleware #{label} raised #{error.class.name}: #{error.message}"
    )
  rescue StandardError
    begin
      $stderr.puts("Middleware #{label} raised #{error.class.name}: #{error.message}")
      $stderr.flush
    rescue StandardError
      # never let logging break the worker
    end
  end
  response.json({ error: "Internal Server Error", status: 500 }, 500)
end

.post_match_middlewareObject

Global middleware that runs after matching, once the matched route's metadata is readable. This is the default.



60
61
62
# File 'lib/tina4/middleware.rb', line 60

def post_match_middleware
  global_middleware.reject { |k| k.respond_to?(:pre_match?) && k.pre_match? }
end

.pre_match_middlewareObject

Global middleware that runs BEFORE route matching.

A middleware opts in by declaring def self.pre_match?; true; end.

NOT before_match? - the hook discovery treats every before_* method as a middleware hook and calls it with (request, response), so that name made the flag itself run as middleware and 500 the request. Everything else stays where it has always run - after matching - so this is additive and no existing middleware changes behaviour.

The split exists because the two groups need opposite things. CORS must run before matching so its headers survive a short-circuited 401/403; a browser that gets a 401 without them reports a CORS error and the real status is invisible. CSRF must run AFTER, because it reads the matched route's metadata to honour a route marked no_auth - PHP shipped exactly that bypass as dead code once, because the metadata was not set yet.



54
55
56
# File 'lib/tina4/middleware.rb', line 54

def pre_match_middleware
  global_middleware.select { |k| k.respond_to?(:pre_match?) && k.pre_match? }
end

.refuse(response) ⇒ Object

The false row of the return-value table, on its own.

A middleware that halts by returning false keeps the response it set; only a response still left default/empty becomes a 403. Public because per-route "filter" middleware (a 2-arg callable returning false, see Tina4::Route#run_middleware) must obey the SAME row as a before_* hook, and the rule should exist exactly once.



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

def refuse(response)
  forbid(response) if default_response?(response)
  response
end

.run_after(middleware_classes, request, response) ⇒ Object

Run all "after" hooks: block-based handlers, then class-based after_* methods (in definition order).

Signature matches Python/PHP/Node orchestrators: pass the list of middleware classes explicitly.

AFTER-ON-4xx RULE (M2, documented + consistent across all 4 frameworks): after_* ALWAYS run even when a before_* short-circuited with status >= 400 and the handler was skipped — so they can still add headers / logging. The dispatcher calls #run_after unconditionally after the before/handler block (including on the 4xx / throw halt path).

M2 — every after_* call is wrapped: a THROW is LOGGED and turns the response into a clean 500, then the REMAINING after_* still run (they may add headers/logging). Never an unhandled crash.

RETURN VALUES: an after_* hook shapes the response the same way a before_* one does — a returned Tina4::Response BECOMES the response, a returned [request, response] pair rebinds both. What it CANNOT do is halt: the handler has already run, so there is nothing left to skip, and stopping the remaining after_* would contradict the AFTER-ON-4xx resilience rule above (they exist to add headers/logging on every path). So false and a >= 400 status are inert here by design.



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/tina4/middleware.rb', line 159

def run_after(middleware_classes, request, response)
  origin = response

  # 1. Block-based after handlers (pattern-matched)
  after_handlers.each do |entry|
    next unless matches_pattern?(request.path, entry[:pattern])

    begin
      entry[:handler].call(request, response)
    rescue StandardError, ScriptError => error
      middleware_500(response, "after handler", error)
    end
  end

  # 2. Class-based middleware: call every after_* method (definition order)
  middleware_classes.each do |klass|
    after_methods_for(klass).each do |method_name|
      begin
        result = klass.send(method_name, request, response)
      rescue StandardError, ScriptError => error
        middleware_500(response, "#{class_label(klass)}.#{method_name}", error)
        next
      end
      if result.is_a?(Tina4::Response)
        response = result
      elsif result.is_a?(Array) && result.length == 2
        request, response = result
      end
    end
  end

  adopt_response(origin, response) unless response.equal?(origin)
  response
end

.run_before(middleware_classes, request, response) ⇒ Object

Run all "before" hooks: block-based handlers, then class-based before_* methods (in definition order).

Signature matches Python/PHP/Node orchestrators: pass the list of middleware classes explicitly.

THE RETURN-VALUE CONTRACT is #apply_before_result below — one table, applied to EVERY before_* hook at EVERY scope. Per-route middleware comes through this same method (Tina4::Route#run_middleware), so there is exactly one implementation of the table, not two.

M2 — visible-but-resilient: every before_* call is wrapped so a THROW never crashes the worker. On a throw the error is LOGGED and the response becomes a clean 500 (Server Error", "status":500), then processing halts (handler skipped) — deterministic, never an unhandled exception. after_* still run on either halt path (see the dispatcher / #run_after docstring).

Returns true on success, or false to halt the request (handler skipped).



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

def run_before(middleware_classes, request, response)
  # The response object the CALLER holds. A hook may hand back a
  # different Response; on a halt that object IS the answer, so its state
  # is adopted onto this one before returning — otherwise the dispatcher
  # would serve the object it still has a reference to and the
  # short-circuit would silently vanish.
  origin = response

  # 1. Block-based before handlers (pattern-matched). These are a
  #    Ruby-only surface (Python/PHP/Node have no block form) and keep
  #    their historical "false halts" contract: a block's value is its
  #    last expression, so reading a returned Response as a
  #    short-circuit would fire on any block ending in a chainable
  #    response call.
  before_handlers.each do |entry|
    next unless matches_pattern?(request.path, entry[:pattern])

    begin
      result = entry[:handler].call(request, response)
    rescue StandardError, ScriptError => error
      middleware_500(response, "before handler", error)
      return false
    end
    return false if result == false
  end

  # 2. Class-based middleware: call every before_* method (definition order)
  middleware_classes.each do |klass|
    before_methods_for(klass).each do |method_name|
      begin
        result = klass.send(method_name, request, response)
      rescue StandardError, ScriptError => error
        middleware_500(response, "#{class_label(klass)}.#{method_name}", error)
        return false
      end

      halt, request, response = apply_before_result(result, request, response)
      next unless halt

      adopt_response(origin, response) unless response.equal?(origin)
      return false
    end
  end

  true
end

.use(klass) ⇒ Object

Register a class-based middleware globally. The class should define static before_* and/or after_* methods.



34
35
36
# File 'lib/tina4/middleware.rb', line 34

def use(klass)
  global_middleware << klass unless global_middleware.include?(klass)
end