Module: Ruact::Server
- Extended by:
- ActiveSupport::Concern
- 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 explicitActionController::InvalidAuthenticityTokenregistration that preempts Rails' defaulthandle_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 viaRuact.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. Hostrescue_fromdeclarations — 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_actionenforcingRuact.config.max_upload_bytesagainst the wireContent-LengthBEFORE 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 afterprotect_from_forgery; no runtime callback-order verifier runs here.
Both bodies live in Ruact::ServerFunctions::ErrorRendering (Story 9.9 —
this concern is now the sole home; the v1 endpoint that previously shared it
was demolished). 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
Constant Summary
Constants included from ValidationErrorsCollector
ValidationErrorsCollector::RESERVED_ASSIGN_KEYS, ValidationErrorsCollector::RUACT_ERRORS_FLASH_KEY
Instance Method Summary collapse
-
#default_render ⇒ Object
Story 9.2 AC2/AC4 (D1) — Bucket-2 success-path negotiation.
-
#redirect_to(options = {}, response_options = {}) ⇒ Object
Story 9.2 AC3 (D2) — on a function-call request,
redirect_tosurfaces 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.
Methods included from ValidationErrorsCollector
Instance Method Details
#default_render ⇒ Object
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).
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/ruact/server.rb', line 195 def default_render(*) return super unless __ruact_function_call? # Story 13.3 (FR98) — the collector's framework-internal ivars (and a stray # dev `@errors`) are dropped from the serialized assigns; Rails' own # `view_assigns` only filters a fixed set of `@_` ivars, not every `@__` # one (see {Ruact::ValidationErrorsCollector::RESERVED_ASSIGN_KEYS}). assigns = view_assigns.except(*ValidationErrorsCollector::RESERVED_ASSIGN_KEYS) # Story 13.3 (FR98, AC3) — when the host opted into the validation-error # round-trip (`ruact_errors(@record)` was called this request), inject the # collected errors under the reserved JSON key `"errors"` alongside the # serialized ivars, even on an otherwise-empty assigns set: an opted-in # success must still surface `{"errors": {}}` so the client's `await` # result carries `result.errors` on a single code path. if __ruact_errors_touched? payload = ServerFunctions::BucketTwoPayload.build( assigns, strict: Ruact.config.strict_serialization ) payload["errors"] = __ruact_errors return render(json: payload) end # An UNTOUCHED collector changes nothing — the 9.2 `204 No Content` # empty-Bucket-2 contract is preserved. 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.
241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/ruact/server.rb', line 241 def redirect_to( = {}, = {}) return super unless __ruact_function_call? raise ActionController::ActionControllerError, "Cannot redirect to nil!" unless raise AbstractController::DoubleRenderError if response_body allow_other_host = .delete(:allow_other_host) location = _compute_redirect_to_location(request, ) _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 |