Class: Axn::Webhooks::Response

Inherits:
Object
  • Object
show all
Defined in:
lib/axn/webhooks/response.rb

Overview

A Rails-agnostic HTTP response value: status + body + headers. Produced by Endpoint#to_response/#challenge_response from the pipeline's Axn::Result. #to_rack renders it as the [status, headers, body] triple Endpoint#call(env) returns.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(status: 200, body: "", headers: {}) ⇒ Response

Returns a new instance of Response.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/axn/webhooks/response.rb', line 13

def initialize(status: 200, body: "", headers: {})
  @status = status
  # deep_freeze (not `.freeze`) so a caller-owned String body isn't frozen in place —
  # `String#to_s` returns self, so `.freeze` would mutate the handler's own string.
  @body = deep_freeze(body.to_s)
  # Keys are lower-cased (Rack 3's SPEC forbids uppercase in response header keys, and
  # Rack::Lint rejects them). Keys AND values are frozen deeply (Array multi-value headers
  # freeze their elements too) so a caller's mutable value can't mutate this rendered-later value.
  # Values carrying CR/LF (or any other byte RFC 7230 forbids) are DROPPED, not rendered:
  # a `respond`/`static_respond`/`unauthorized_headers` declaration that echoes request data
  # into a header would otherwise let a sender inject headers or split the response. Dropped
  # rather than raised so a rendering mistake degrades to a missing header instead of a 500,
  # matching what the outbound half already does with a subscriber's custom headers.
  @headers = headers.each_with_object({}) do |(key, value), frozen|
    safe = sanitize_header(key, value)
    next if safe.nil?

    frozen[key.to_s.downcase.freeze] = deep_freeze(safe)
  end.freeze
  freeze
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



11
12
13
# File 'lib/axn/webhooks/response.rb', line 11

def body
  @body
end

#headersObject (readonly)

Returns the value of attribute headers.



11
12
13
# File 'lib/axn/webhooks/response.rb', line 11

def headers
  @headers
end

#statusObject (readonly)

Returns the value of attribute status.



11
12
13
# File 'lib/axn/webhooks/response.rb', line 11

def status
  @status
end

Class Method Details

.ack(status: 200, headers: {}) ⇒ Object



61
# File 'lib/axn/webhooks/response.rb', line 61

def self.ack(status: 200, headers: {}) = new(status:, headers:)

.json(body, status: 200, headers: {}) ⇒ Object

A Hash/Array body is JSON-encoded; a String is assumed pre-serialized and passed through.



72
73
74
75
# File 'lib/axn/webhooks/response.rb', line 72

def self.json(body, status: 200, headers: {})
  body = JSON.generate(body) unless body.is_a?(String)
  new(status:, body:, headers: { "content-type" => "application/json" }.merge(headers))
end

.service_unavailable(retry_after: nil) ⇒ Object



82
83
84
85
# File 'lib/axn/webhooks/response.rb', line 82

def self.service_unavailable(retry_after: nil)
  headers = retry_after ? { "retry-after" => retry_after.to_s } : {}
  new(status: 503, headers:)
end

.text(body, status: 200, headers: {}) ⇒ Object



63
64
65
# File 'lib/axn/webhooks/response.rb', line 63

def self.text(body, status: 200, headers: {})
  new(status:, body:, headers: { "content-type" => "text/plain" }.merge(headers))
end

.valid_status?(value) ⇒ Boolean

A plausible HTTP status: an Integer inside the range HTTP defines. Shared by the unparseable_status config setting and the per-endpoint dispatch unparseable_status:, so both reject the same values against the same bound.

Returns:

  • (Boolean)


80
# File 'lib/axn/webhooks/response.rb', line 80

def self.valid_status?(value) = value.is_a?(Integer) && (200..599).cover?(value)

.xml(body, status: 200, headers: {}) ⇒ Object



67
68
69
# File 'lib/axn/webhooks/response.rb', line 67

def self.xml(body, status: 200, headers: {})
  new(status:, body:, headers: { "content-type" => "application/xml" }.merge(headers))
end

Instance Method Details

#==(other) ⇒ Object



92
93
94
# File 'lib/axn/webhooks/response.rb', line 92

def ==(other)
  other.is_a?(self.class) && status == other.status && body == other.body && headers == other.headers
end

#to_rackObject

[status, headers, body] — the Rack app return contract. Headers are already lower-cased (see #initialize); body is wrapped in an Array, Rack's documented minimal body contract. Return a mutable copy of headers so Rails middleware can add headers (e.g., ETag). Array header values (multi-value headers like Set-Cookie) are duped to be mutable so middleware like Rack::Utils.set_cookie_header! can append; String values pass through. Rack 3 requires Array headers, not newline-joined Strings.



102
# File 'lib/axn/webhooks/response.rb', line 102

def to_rack = [status, headers.transform_values { |value| value.is_a?(Array) ? value.dup : value }, [body]]

#with_status(status) ⇒ Object

The same body and headers under a different status. The unparseable-body mapping needs it: a declared static_respond block picked its status for the success path, but the gem owns the outcome->status mapping, so the body a vendor keys on survives and only the status is restamped.



90
# File 'lib/axn/webhooks/response.rb', line 90

def with_status(status) = self.class.new(status:, body:, headers:)