Module: Ruact::ServerFunctions::ErrorPayload

Defined in:
lib/ruact/server_functions/error_payload.rb

Overview

Story 8.4 — Builds the structured JSON body returned by Ruact::ServerFunctions::ErrorRendering#__ruact_render_action_error for any server-action exception that bubbles past a host’s ‘rescue_from` chain.

The function is pure (no ‘Rails.env`, no `Ruact.config` reads) — the caller resolves `mode` (`:development` or `:production`) and passes it in. That keeps the module trivially testable without stubbing Rails env.

In ‘:development` mode the payload carries the full surface: action name, error class, message, split backtrace (first 25 frames per bucket), contextual suggestion, and (for `ActiveRecord::RecordInvalid`) the model’s ‘full_messages`.

In ‘:production` mode the payload is reduced to four baseline keys: `_ruact_server_action_error`, `action_name`, `error_class`, `message`. React components can render their own UI from those four fields without any accidental backtrace leakage on the wire.

Constant Summary collapse

MAX_FRAMES_PER_BUCKET =

Maximum frames preserved per bucket. The full backtrace is still in the server log; the wire payload is for the overlay, which is unreadable past a couple of dozen frames anyway.

25

Class Method Summary collapse

Class Method Details

.build(action_name:, error:, mode:) ⇒ Hash{String=>Object}

Parameters:

  • action_name (Symbol, String)
  • error (Exception)
  • mode (Symbol)

    :development or :production

Returns:

  • (Hash{String=>Object})


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ruact/server_functions/error_payload.rb', line 35

def self.build(action_name:, error:, mode:)
  # Pitfall #5: defensive dup against frozen-string `Exception#message`
  # implementations.
  message = error.message.to_s.dup
  payload = {
    "_ruact_server_action_error" => true,
    "action_name" => action_name.to_s,
    "error_class" => error.class.name,
    "message" => message
  }
  return payload if mode == :production

  frames = BacktraceCleaner.split(error.backtrace)
  payload["app_frames"] = frames[:app].first(MAX_FRAMES_PER_BUCKET)
  payload["gem_frames"] = frames[:gem].first(MAX_FRAMES_PER_BUCKET)
  payload["suggestion"] = ErrorSuggestion.for(error)
  validation_errors = extract_validation_errors(error)
  payload["validation_errors"] = validation_errors if validation_errors
  upload_limit = extract_upload_limit(error)
  payload["upload_limit"] = upload_limit if upload_limit
  payload
end