Class: Axn::Webhooks::Request
- Inherits:
-
Object
- Object
- Axn::Webhooks::Request
- Defined in:
- lib/axn/webhooks/request.rb
Overview
A Rails-agnostic view of an inbound webhook request. Verifiers and dispatchers read only from this object, so the same pipeline works behind a Rack mount, a controller, or a plain test constructor. Header lookup is case-insensitive.
Defined Under Namespace
Classes: ParamsResult
Instance Attribute Summary collapse
-
#http_method ⇒ Object
readonly
Returns the value of attribute http_method.
-
#params_error ⇒ Object
readonly
The exception raised while parsing the query/form params, or nil.
-
#params_reads ⇒ Object
readonly
How many times #params has been read.
-
#raw_body ⇒ Object
readonly
Returns the value of attribute raw_body.
-
#url ⇒ Object
readonly
Returns the value of attribute url.
Class Method Summary collapse
-
.from_rack(env) ⇒ Object
Build a Request from a Rack env, capturing the exact pristine body bytes — this (not a controller's already-parsed params) is why the spec chose a Rack mount over a controller concern (see "## Packaging" in the design spec).
Instance Method Summary collapse
- #header(name) ⇒ Object
-
#initialize(raw_body:, headers: {}, params: {}, url: nil, http_method: "POST", params_error: nil) ⇒ Request
constructor
A new instance of Request.
-
#inspect ⇒ Object
raw_bodyandheadersare attacker-controlled webhook payloads of unknown sensitivity (bank account numbers, API credentials, mailing addresses have all shown up in the wild) — never render them. -
#params ⇒ Object
Always a Hash, never raises — this is reachable BEFORE verification (a custom verifier or a
challenge_requiredpredicate may read it), where a raise would let an unauthenticated sender turn a 401 into a reported 500. -
#pretty_print(printer) ⇒ Object
pp/PP does not call #inspect by default (Kernel#pretty_print walks instance variables directly), so without this overridepp requestwould leak the same fields #inspect redacts.
Constructor Details
#initialize(raw_body:, headers: {}, params: {}, url: nil, http_method: "POST", params_error: nil) ⇒ Request
Returns a new instance of Request.
13 14 15 16 17 18 19 20 21 22 23 |
# File 'lib/axn/webhooks/request.rb', line 13 def initialize(raw_body:, headers: {}, params: {}, url: nil, http_method: "POST", params_error: nil) @raw_body = raw_body.frozen? ? raw_body : raw_body.dup.freeze @headers = (headers || {}).each_with_object({}) { |(k, v), h| h[k.to_s.downcase] = v } @params = (params || {}).dup.freeze # The failure `extract_params` swallowed, if any — see #params. Kept so the POST-verification # parse step can still see it, without it ever reaching the pre-verification path. @params_error = params_error @params_reads = 0 @url = url @http_method = http_method.to_s.upcase end |
Instance Attribute Details
#http_method ⇒ Object (readonly)
Returns the value of attribute http_method.
25 26 27 |
# File 'lib/axn/webhooks/request.rb', line 25 def http_method @http_method end |
#params_error ⇒ Object (readonly)
The exception raised while parsing the query/form params, or nil. Public so Dispatch can surface it AFTER verification (see #params).
29 30 31 |
# File 'lib/axn/webhooks/request.rb', line 29 def params_error @params_error end |
#params_reads ⇒ Object (readonly)
How many times #params has been read. A COUNT, not a flag, so a caller can scope the
question to a window rather than the request's whole lifetime — Dispatch compares it either
side of the parse call. A lifetime flag was wrong: a custom verifier or a
challenge_required predicate legitimately reads params BEFORE the parse step, and counting
that read re-opened the downgrade the gate exists to prevent (Codex review).
36 37 38 |
# File 'lib/axn/webhooks/request.rb', line 36 def params_reads @params_reads end |
#raw_body ⇒ Object (readonly)
Returns the value of attribute raw_body.
25 26 27 |
# File 'lib/axn/webhooks/request.rb', line 25 def raw_body @raw_body end |
#url ⇒ Object (readonly)
Returns the value of attribute url.
25 26 27 |
# File 'lib/axn/webhooks/request.rb', line 25 def url @url end |
Class Method Details
.from_rack(env) ⇒ Object
Build a Request from a Rack env, capturing the exact pristine body bytes — this (not a controller's already-parsed params) is why the spec chose a Rack mount over a controller concern (see "## Packaging" in the design spec).
rack.input is OPTIONAL under Rack 3 (it was mandatory in Rack 2), so a bodyless request may
omit the key entirely — Rack::MockRequest.env_for does exactly that, which is what a Rails
integration/request spec builds. Treat a missing input as an empty body rather than a
malformed env: the GET challenge handshake (Nylas, Meta) is bodyless by definition, so
fetching here would 500 the very handshake challenge exists to serve.
We rewind BEFORE reading, not only after. Under Rack 3, Rack::Request#POST no longer
rewinds rack.input after parsing a form-urlencoded body — and Rails' default middleware
stack runs Rack::MethodOverride (which calls #POST looking for _method) ahead of the
router. So by the time a mounted endpoint runs, the input of every form-encoded POST is
already at EOF and reads as "". That silently empties raw_body AND params for exactly the
vendors that post forms (Twilio, Slack), breaking dispatch and signature verification alike.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/axn/webhooks/request.rb', line 81 def self.from_rack(env) input = env["rack.input"] rewind(input) raw_body = input&.read || "" rewind(input) # courtesy for anything downstream of us content_type = env["CONTENT_TYPE"] params_result = extract_params(env, raw_body, content_type) new( raw_body:, headers: extract_headers(env), params: params_result.value, params_error: params_result.error, url: extract_url(env), http_method: env["REQUEST_METHOD"], ) end |
Instance Method Details
#header(name) ⇒ Object
47 48 49 |
# File 'lib/axn/webhooks/request.rb', line 47 def header(name) @headers[name.to_s.downcase] end |
#inspect ⇒ Object
raw_body and headers are attacker-controlled webhook payloads of unknown sensitivity
(bank account numbers, API credentials, mailing addresses have all shown up in the wild) —
never render them. This is the one place that matters: axn's auto-logging, exception
reports, and any other caller that inspects a Request all go through #inspect.
55 56 57 |
# File 'lib/axn/webhooks/request.rb', line 55 def inspect "#<#{self.class.name} #{http_method} #{url} raw_body=[REDACTED] (#{raw_body.bytesize}b) headers=[REDACTED]>" end |
#params ⇒ Object
Always a Hash, never raises — this is reachable BEFORE verification (a custom verifier or a
challenge_required predicate may read it), where a raise would let an unauthenticated
sender turn a 401 into a reported 500. The swallowed failure is not lost: it is kept on
#params_error and re-raised by the parse step, which runs only after verification.
42 43 44 45 |
# File 'lib/axn/webhooks/request.rb', line 42 def params @params_reads += 1 @params end |
#pretty_print(printer) ⇒ Object
pp/PP does not call #inspect by default (Kernel#pretty_print walks instance variables
directly), so without this override pp request would leak the same fields #inspect redacts.
61 62 63 |
# File 'lib/axn/webhooks/request.rb', line 61 def pretty_print(printer) printer.text(inspect) end |