Discourse Onboarding LabChapter II — Rails Controllers: The Request Gateway0/6Contents
Chapter II

Rails Controllers: The Request Gateway

Every AJAX request from the Ember frontend lands in a Rails controller that inherits from ApplicationController, whose before_action/after_action chain enforces CSRF protection, readonly mode, login requirements, and locale before any subclass action runs. Subclass controllers (like NotificationsController) add their own before_actions, load ActiveRecord models scoped through a guardian authorization object, and hand the results to render_serialized/render_json_dump, which serialize models into JSON and write the response Ember's ajax() helper is waiting on.


From Ember AJAX call to JSON response
CSRF token initializer…stance-initializers/csrf-token.js
ApplicationController…rollers/application_controller.rb
Subclass controller (e.g. NotificationsController)…llers/notifications_controller.rb
Serializer + JSON render…rollers/application_controller.rb
Ember ajax() helperfrontend/discourse/app/lib/ajax.js
Figure I · Click a node to see what it does, where it lives in the code, and its labeled connections — the annotation opens beside it.
Figure II · pipelineApplicationController's callback chain
6 stages
Press play — or step through the pipeline stage by stage.
walkthrough

CSRF protection: protect_from_forgery + handle_unverified_request

app/controllers/application_controller.rb · lines 2234
line 22

protect_from_forgery turns on Rails' built-in CSRF protection for every action in ApplicationController and its subclasses, requiring a valid authenticity/X-CSRF-Token on state-changing requests.

lines 27–34

handle_unverified_request overrides Rails' default forgery-failure hook. Instead of silently continuing with a blank session, Discourse clears the current user and renders a 403 "BAD CSRF" body — unless the request authenticated via API key, which is itself a secret that makes CSRF checks redundant.

  protect_from_forgery  # Default Rails 3.2 lets the request through with a blank session  #  we are being more pedantic here and nulling session / current_user  #  and then raising a CSRF exception  def handle_unverified_request    # NOTE: API key is secret, having it invalidates the need for a CSRF token    unless is_api? || is_user_api?      super      clear_current_user      render plain: "[\"BAD CSRF\"]", status: :forbidden    end  end
step 1 of 2
  def index    user =      if params[:username] && !params[:recent]        user_record = User.find_by(username: params[:username].to_s)        raise Discourse::NotFound if !user_record        user_record      else        current_user      end    guardian.ensure_can_see_notifications!(user)    if notification_types = params[:filter_by_types]&.split(",").presence      notification_types.map! do |type|        Notification.types[type.to_sym] ||          (raise Discourse::InvalidParameters.new("invalid notification type: #{type}"))      end    end
app/controllers/notifications_controller.rb · lines 1027
Figure III · Actions load ActiveRecord models directly (User.find_by) and then call guardian.ensure_can_see_notifications! before touching any data — the guardian object, built once per request in ApplicationController#guardian, is Discourse's authorization layer and is checked in nearly every action.
  def serialize_data(obj, serializer, opts = nil)    # If it's an array, apply the serializer as an each_serializer to the elements    serializer_opts = { scope: guardian }.merge!(opts || {})    if obj.respond_to?(:to_ary)      serializer_opts[:each_serializer] = serializer      ActiveModel::ArraySerializer.new(obj.to_ary, serializer_opts).as_json    else      serializer.new(obj, serializer_opts).as_json    end  end  # This is odd, but it seems that in Rails `render json: obj` is about  # 20% slower than calling MultiJSON.dump ourselves. I'm not sure why  # Rails doesn't call MultiJson.dump when you pass it json: obj but  # it seems we don't need whatever Rails is doing.  def render_serialized(obj, serializer, opts = nil)    render_json_dump(serialize_data(obj, serializer, opts), opts)  end  def render_json_dump(obj, opts = nil)    opts ||= {}    if opts[:rest_serializer]      obj["__rest_serializer"] = "1"      opts.each { |k, v| obj[k] = v if k.to_s.start_with?("refresh_") }      obj["extras"] = opts[:extras] if opts[:extras]      obj["meta"] = opts[:meta] if opts[:meta]    end    render json: MultiJson.dump(obj), status: opts[:status] || 200  end
app/controllers/application_controller.rb · lines 488518
Figure IV · render_serialized wraps serialize_data, which scopes every serializer to guardian (so each object is serialized under the current user's permissions), then hands the result to render_json_dump, which dumps JSON manually via MultiJson rather than Rails' render json: for performance.
checkpoint

Check your understanding

Answered in place — nothing is graded, everything is explained. 0 / 3 passed

Which before_action actually raises Discourse::ReadOnly to stop a mutating request while the site is in readonly mode?

What does ApplicationController do when a non-API request fails Rails' CSRF check?

Where do controller actions get their authorization checks and serializer scoping from?