Class: Restless::Middleware

Inherits:
Object
  • Object
show all
Defined in:
lib/restless/rack.rb

Overview

Rack middleware. One interface covers Rails, Sinatra, Hanami, Grape, Roda and anything else that speaks Rack.

CONTRACT.md section 14 is explicit that adapters are per-language and not standardised, so this does NOT reproduce the Node SDK's duck-typed universal middleware or its framework list. Rack is the one interface worth having in Ruby.

SAFETY-001 governs everything below: no code path here may propagate an exception into the customer's app or response lifecycle. The only exception deliberately re-raised is the customer's own.

Defined Under Namespace

Classes: Factory, RequestInfo

Constant Summary collapse

MAX_CAPTURE_BYTES =

SAFETY-007. Bodies over this are recorded WITHOUT a body; headers are still stamped. Capture must never buffer unboundedly.

1024 * 1024
STREAMING_TYPES =

SAFETY-007. Streaming responses are passed straight through, never buffered.

%w[text/event-stream].freeze
SKIPPED_REQUEST_TYPES =

SAFETY-006. A serialized parse of multipart is meaningless.

%w[multipart/form-data].freeze
ROUTE_ENV_KEYS =

Framework hooks that carry the matched route template, most specific first. Override wholesale with the route: lambda.

%w[
  restless.route
  sinatra.route
  action_dispatch.route_uri_pattern
  grape.routing_args
].freeze
EXCEPTION_ENV_KEYS =

Frameworks that catch the exception themselves still leave it in the env, which is the only way to reach the stack fingerprint strategy for a handled 500.

%w[
  restless.exception
  sinatra.error
  action_dispatch.exception
  rack.exception
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, client, options = {}) ⇒ Middleware

Returns a new instance of Middleware.



107
108
109
110
111
112
113
114
# File 'lib/restless/rack.rb', line 107

def initialize(app, client, options = {})
  @app = app
  @client = client
  @engine = client.engine
  @route_resolver = options[:route]
  @capture_request_body =
    options.key?(:capture_request_body) ? options[:capture_request_body] : true
end

Class Method Details

.factory(client, **options) ⇒ Object



67
68
69
# File 'lib/restless/rack.rb', line 67

def self.factory(client, **options)
  Factory.new(client, options)
end

.full_url(env) ⇒ Object



160
161
162
163
164
165
166
167
# File 'lib/restless/rack.rb', line 160

def self.full_url(env)
  scheme = env["rack.url_scheme"] || "http"
  host = env["HTTP_HOST"] || "#{env['SERVER_NAME']}:#{env['SERVER_PORT']}"
  url = +"#{scheme}://#{host}#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
  query = env["QUERY_STRING"].to_s
  url << "?" << query unless query.empty?
  url
end

.normalize_route_pattern(pattern) ⇒ Object

GET /pets/:id (Sinatra) and /pets/:id(.:format) (Rails) both become /pets/{id}, which is what every other Restless SDK reports for the same endpoint. Without that the same API produces two different routePattern values depending on the language it was written in.



173
174
175
176
177
178
179
180
181
# File 'lib/restless/rack.rb', line 173

def self.normalize_route_pattern(pattern)
  route = Text.ws_trim(pattern.to_s)
  return nil if route.empty?

  route = route.sub(/\A[A-Z]+[ \t]+/, "")     # strip the leading method
  route = route.sub(/\(\.:format\)\z/, "")    # Rails' optional format
  route = route.gsub(/:([A-Za-z_][A-Za-z0-9_]*)/, '{\1}')
  route.empty? ? nil : route
end

Instance Method Details

#call(env) ⇒ Object



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
# File 'lib/restless/rack.rb', line 116

def call(env)
  started_at = Time.now
  clock = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  request = RequestInfo.new(env)

  setup = safely({}) { @engine.resolve(request) }
  our_id = RequestId.new_request_id

  block = safely(nil) { CaptureEngine.resolve_block(setup["block"]) }
  if block
    # SETUP-004: reject before the handler runs.
    return finish(env, setup, our_id, started_at, clock, block_response(block),
                  request_body: capture_request_body(env), stack_frame: nil)
  end

  request_body = capture_request_body(env)

  begin
    status, headers, body = @app.call(env)
  rescue Exception => e # rubocop:disable Lint/RescueException
    # The customer's exception. Capture it with the stack so the
    # fingerprint keys on the RAISING method (FP-043), then re-raise so
    # their own error handling is completely unaffected.
    frame = safely(nil) { StackFrames.from_exception(e) }
    safely(nil) do
      finish(env, setup, our_id, started_at, clock,
             [500, { "content-type" => "application/json" },
              [JSON.generate({ "error" => "Internal Server Error" })]],
             request_body: request_body, stack_frame: frame, inject: false)
    end
    raise
  end

  # A framework that handled the exception itself still left it here.
  frame = nil
  if status.to_i >= 500
    error = EXCEPTION_ENV_KEYS.map { |k| env[k] }.find { |v| v.respond_to?(:backtrace) }
    frame = safely(nil) { StackFrames.from_exception(error) } if error
  end

  finish(env, setup, our_id, started_at, clock, [status, headers, body],
         request_body: request_body, stack_frame: frame)
end