Module: Nfe::ErrorFactory
- Defined in:
- lib/nfe/error_factory.rb,
sig/nfe/error_factory.rbs
Overview
Translates HTTP responses and network exceptions into the typed Error hierarchy.
The transport returns 4xx/5xx as Http::Response objects (never raising); the resource layer feeds those to ErrorFactory.from_response to obtain the right error class. Network failures the transport raised are normalized via ErrorFactory.from_network_error.
The extracted message echoes server-controlled input, so it is capped to a bounded length and stripped of control characters before reaching the error — an attacker-controlled body must not flood logs or inject terminal escape sequences.
Constant Summary collapse
- MAX_MESSAGE_LENGTH =
Maximum length of a message extracted from a response body.
500- MESSAGE_KEYS =
Body keys searched (in order) for a human-readable message.
%w[message error detail details].freeze
- ERROR_CODE_KEYS =
Body keys searched (in order) for a machine-readable error code.
%w[code errorCode error_code].freeze
- CONTROL_CHARS =
ASCII control characters (C0 range plus DEL) scrubbed from messages.
/[\x00-\x1f\x7f]/
Class Method Summary collapse
- .error_class_for(status) ⇒ Object private
- .extract_error_code(parsed) ⇒ Object private
- .extract_message(parsed) ⇒ Object private
- .extract_retry_after(response) ⇒ Object private
-
.from_network_error(exception) ⇒ Nfe::ApiConnectionError
Normalize a network exception raised by the transport.
-
.from_response(response) ⇒ Nfe::Error
Build the appropriate Error subclass for an HTTP response.
- .message_from_errors(errors) ⇒ Object private
- .message_from_keys(parsed) ⇒ Object private
- .parse_body(body) ⇒ Object private
- .sanitize_message(raw) ⇒ Object private
- .timeout?(exception) ⇒ Boolean private
Instance Method Summary collapse
- #self?.error_class_for ⇒ singleton(Nfe::Error)
- #self?.extract_error_code ⇒ String?
- #self?.extract_message ⇒ String?
- #self?.extract_retry_after ⇒ Integer?
- #self?.from_network_error ⇒ Nfe::ApiConnectionError
- #self?.from_response ⇒ Nfe::Error
- #self?.message_from_errors ⇒ Object
- #self?.message_from_keys ⇒ String?
- #self?.parse_body ⇒ Object
- #self?.sanitize_message ⇒ String?
- #self?.timeout? ⇒ Boolean
Class Method Details
.error_class_for(status) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/nfe/error_factory.rb', line 77 def error_class_for(status) case status when 401 then AuthenticationError when 403 then AuthorizationError when 404 then NotFoundError when 409 then ConflictError when 429 then RateLimitError when 400..499 then InvalidRequestError when 500..599 then ServerError else status >= 500 ? ServerError : InvalidRequestError end end |
.extract_error_code(parsed) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
141 142 143 144 145 146 147 148 149 |
# File 'lib/nfe/error_factory.rb', line 141 def extract_error_code(parsed) return nil unless parsed.is_a?(Hash) ERROR_CODE_KEYS.each do |key| value = parsed[key] return value.to_s if value.is_a?(String) || value.is_a?(Integer) end nil end |
.extract_message(parsed) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
101 102 103 104 105 106 |
# File 'lib/nfe/error_factory.rb', line 101 def (parsed) return nil unless parsed.is_a?(Hash) raw = (parsed) || (parsed["errors"]) (raw) end |
.extract_retry_after(response) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
152 153 154 155 156 157 158 159 |
# File 'lib/nfe/error_factory.rb', line 152 def extract_retry_after(response) value = response.header("retry-after") return nil if value.nil? Integer(value, 10) rescue ArgumentError, TypeError nil end |
.from_network_error(exception) ⇒ Nfe::ApiConnectionError
Normalize a network exception raised by the transport.
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/nfe/error_factory.rb', line 65 def from_network_error(exception) klass = timeout?(exception) ? TimeoutError : ApiConnectionError error = klass.new(exception.) # Preserve the original exception as cause for debugging. begin raise error, error., cause: exception rescue klass => e e end end |
.from_response(response) ⇒ Nfe::Error
Build the appropriate Nfe::Error subclass for an HTTP response.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/nfe/error_factory.rb', line 37 def from_response(response) status = response.status parsed = parse_body(response.body) = (parsed) || "API request failed with HTTP #{status}" error_code = extract_error_code(parsed) request_id = response.header("x-request-id") || response.header("x-correlation-id") kwargs = { status_code: status, request_id: request_id, error_code: error_code, response_body: response.body, response_headers: response.headers } klass = error_class_for(status) if klass == RateLimitError RateLimitError.new(, retry_after: extract_retry_after(response), **kwargs) else klass.new(, **kwargs) end end |
.message_from_errors(errors) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
118 119 120 121 122 123 124 125 126 |
# File 'lib/nfe/error_factory.rb', line 118 def (errors) case errors when String errors when Array first = errors[0] first.is_a?(Hash) ? first["message"] : first end end |
.message_from_keys(parsed) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
109 110 111 112 113 114 115 |
# File 'lib/nfe/error_factory.rb', line 109 def (parsed) MESSAGE_KEYS.each do |key| value = parsed[key] return value if value.is_a?(String) && !value.empty? end nil end |
.parse_body(body) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
92 93 94 95 96 97 98 |
# File 'lib/nfe/error_factory.rb', line 92 def parse_body(body) return nil if body.nil? || body.empty? JSON.parse(body) rescue JSON::ParserError nil end |
.sanitize_message(raw) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
129 130 131 132 133 134 135 136 137 138 |
# File 'lib/nfe/error_factory.rb', line 129 def (raw) return nil if raw.nil? # Scrub ASCII control characters (incl. ESC) so an attacker-controlled # body cannot inject terminal escape sequences, then cap the length. text = raw.to_s.gsub(CONTROL_CHARS, " ").strip return nil if text.empty? text.length > MAX_MESSAGE_LENGTH ? "#{text[0, MAX_MESSAGE_LENGTH]}..." : text end |
.timeout?(exception) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
162 163 164 165 166 |
# File 'lib/nfe/error_factory.rb', line 162 def timeout?(exception) timeout_class_names = %w[Net::OpenTimeout Net::ReadTimeout Timeout::Error] ancestor_names = exception.class.ancestors.map(&:to_s) timeout_class_names.intersect?(ancestor_names) end |
Instance Method Details
#self?.error_class_for ⇒ singleton(Nfe::Error)
10 |
# File 'sig/nfe/error_factory.rbs', line 10
def self?.error_class_for: (Integer status) -> singleton(Nfe::Error)
|
#self?.extract_error_code ⇒ String?
16 |
# File 'sig/nfe/error_factory.rbs', line 16
def self?.extract_error_code: (untyped parsed) -> String?
|
#self?.extract_message ⇒ String?
12 |
# File 'sig/nfe/error_factory.rbs', line 12
def self?.extract_message: (untyped parsed) -> String?
|
#self?.extract_retry_after ⇒ Integer?
17 |
# File 'sig/nfe/error_factory.rbs', line 17
def self?.extract_retry_after: (untyped response) -> Integer?
|
#self?.from_network_error ⇒ Nfe::ApiConnectionError
9 |
# File 'sig/nfe/error_factory.rbs', line 9
def self?.from_network_error: (Exception exception) -> Nfe::ApiConnectionError
|
#self?.from_response ⇒ Nfe::Error
8 |
# File 'sig/nfe/error_factory.rbs', line 8
def self?.from_response: (untyped response) -> Nfe::Error
|
#self?.message_from_errors ⇒ Object
14 |
# File 'sig/nfe/error_factory.rbs', line 14
def self?.message_from_errors: (untyped errors) -> untyped
|
#self?.message_from_keys ⇒ String?
13 |
# File 'sig/nfe/error_factory.rbs', line 13
def self?.message_from_keys: (Hash[String, untyped] parsed) -> String?
|
#self?.parse_body ⇒ Object
11 |
# File 'sig/nfe/error_factory.rbs', line 11
def self?.parse_body: (String? body) -> untyped
|
#self?.sanitize_message ⇒ String?
15 |
# File 'sig/nfe/error_factory.rbs', line 15
def self?.sanitize_message: (untyped raw) -> String?
|
#self?.timeout? ⇒ Boolean
18 |
# File 'sig/nfe/error_factory.rbs', line 18
def self?.timeout?: (Exception exception) -> bool
|