Module: Ruact::Server

Extended by:
ActiveSupport::Concern
Includes:
Ruact::ServerFunctions::ErrorRendering
Defined in:
lib/ruact/server.rb

Overview

Story 9.1 (route-driven redesign, Phase A) — the v2 server-functions marker concern.

class PostsController < ApplicationController
  include Ruact::Server        # the ONLY marker — no per-action DSL

  def create                   # non-GET routed action → callable server function
    @post = Post.create!(title: params[:title])
    redirect_to @post
  end
end

Per the 2026-06-02 ADR addendum (Story 9-0, ‘docs/internal/decisions/server-functions-api.md`), exposure is decided by `routes.rb` — the Story 9.3 codegen reads `Rails.application.routes` filtered to non-GET routes on controllers that include this concern. The concern itself registers NOTHING and emits NOTHING; at this story it is a pure marker plus the new home of the two salvaged Epic-8 subsystems, running on the host controller’s own callback chain:

  • **Story 8.4 structured-error chain** — ‘rescue_from StandardError` (+ an explicit `ActionController::InvalidAuthenticityToken` registration that preempts Rails’ default ‘handle_unverified_request`, Pitfall #1). On function-call requests (#__ruact_function_call?) an uncaught exception renders the structured JSON payload (discriminator `_ruact_server_action_error: true`, four baseline fields, dev/prod split via `Ruact.config.dev_error_payload_enabled`, status mapping 422/403/ 413/500). On every other request shape — including GET/HEAD requests regardless of their Accept header — the handler re-raises, so GET pages, `default_render`, and Phase-1 behavior stay byte-for-byte untouched. Host `rescue_from` declarations — whether inherited from a parent class or declared in the host’s own body — keep precedence: the chain only catches what the host did not.

  • **Story 8.5 upload guard** — ‘prepend_before_action` enforcing `Ruact.config.max_upload_bytes` against the wire `Content-Length` BEFORE Rack’s multipart parser. The three carve-outs are preserved: nil limit, non-multipart/urlencoded content type, absent Content-Length. New here (D2): GET/HEAD requests skip the guard entirely. The 413 renders structured for ALL request shapes (D1) —a meaningful 413 beats a re-raised 500 for native form submits too. Contract simplification: the concern assumes the host includes it after ‘protect_from_forgery`; no runtime callback-order verifier runs here.

Both bodies live in Ruact::ServerFunctions::ErrorRendering, shared with the v1 Ruact::ServerFunctions::EndpointController during the strangler-fig transition so the wire contract is identical by construction. Dual-bucket response negotiation (ivar serialization, ‘$redirect`, 204, `Vary: Accept`) is Story 9.2; this concern only contributes the discrimination predicate 9.2 will reuse.

Defined Under Namespace

Modules: ClassMethods

Instance Method Summary collapse

Instance Method Details

#default_renderObject

Story 9.2 AC2/AC4 (D1) — Bucket-2 success-path negotiation. When the action finished without an explicit render on a function-call request (#__ruact_function_call? — ‘Accept: application/json`, non-GET), serialize the action’s exposed instance variables (Rails ‘view_assigns`, verbatim —the same set a view would see) as a JSON object keyed by ivar name, or `204 No Content` when none were set. Any other request shape falls through to `super` so Bucket-1 rendering — the host’s ‘Ruact::Controller` Flight re-render, then Rails — is byte-for-byte unchanged (AC1).

The exposed-ivar set is Rails’ own ‘view_assigns` with no custom filtering: Rails already excludes its protected `@_`-prefixed internals (including the CSRF `@_marked_for_same_origin_verification` flag), so what remains is exactly what the action assigned. Each value is serialized through the `ruact_props` / `Ruact::Serializable` / `strict_serialization` rules (Ruact::ServerFunctions::BucketTwoPayload); a single ivar stays keyed (no magic unwrap).



194
195
196
197
198
199
200
201
202
203
# File 'lib/ruact/server.rb', line 194

def default_render(*)
  return super unless __ruact_function_call?

  assigns = view_assigns
  return head(:no_content) if assigns.empty?

  render json: ServerFunctions::BucketTwoPayload.build(
    assigns, strict: Ruact.config.strict_serialization
  )
end

#redirect_to(options = {}, response_options = {}) ⇒ Object

Story 9.2 AC3 (D2) — on a function-call request, ‘redirect_to` surfaces as a JSON redirect directive — body `“$redirect” => “<path>”` (the runtime follows it client-side; re-targeting/following is Story 9.3) instead of a 302 or a Flight redirect row. Any other request shape falls through to `super` so the Bucket-1 Flight redirect row / Rails 302 is unchanged (AC1).

Review round 1 — reuses Rails’ OWN redirect machinery (‘_compute_redirect_to_location`, `_ensure_url_is_http_header_safe`, `_enforce_open_redirect_protection`) so the nil-check, header-safety, and open-redirect protection (`allow_other_host` / `raise_on_open_redirects`) match Bucket 1 / stock Rails exactly — a cross-host `redirect_to` raises `UnsafeRedirectError` instead of leaking an external `$redirect`. Same- origin targets collapse to a path; an allowed external origin keeps the absolute URL.

Raises:

  • (ActionController::ActionControllerError)


219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/ruact/server.rb', line 219

def redirect_to(options = {}, response_options = {})
  return super unless __ruact_function_call?

  raise ActionController::ActionControllerError, "Cannot redirect to nil!" unless options
  raise AbstractController::DoubleRenderError if response_body

  allow_other_host = response_options.delete(:allow_other_host)
  location = _compute_redirect_to_location(request, options)
  _ensure_url_is_http_header_safe(location)
  location = _enforce_open_redirect_protection(location, allow_other_host: allow_other_host)

  render json: { "$redirect" => __ruact_redirect_path(location) }
end