ApplicationController::RenderEmpty in ThingController#index

Just wondering if anyone has any advice for this error?
It may be I’m doing a chunk of the Rails side of things wrong
This is happening when I refresh the page on a working part of the application.
Googling it brings up only its entry in the Discourse source

I need Rails Routes for the Thing I’m handling
Therefore I have to have a ThingController.rb <-- is this true?
Currently ThingController.rb is very minimal, since the actual content here is a Topic List and is being rendered as such by Ember and TopicController.

class ThingController < ApplicationController
  def index
  end

  def sent
  end
end

Anyone any thoughts here?

Your controller inherits from the ApplicationController. In the application controller there are a number of methods that run on the before_action hook. This means they will run on every action in your controller (unless you skip them).

This particular before_action method is this

  def check_xhr
    # bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying
    return if !request.get? && (is_api? || is_user_api?)
    raise ApplicationController::RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?)
  end

Basically if the request is not a JSON request, or via the API, it checks that the request is a properly formed request from the javascript client, using an alias of this ROR method: https://apidock.com/rails/ActionController/Request/xml_http_request%3F

You will sometimes find in Discourse that this before_action method is skipped (a fair bit actually). In your case, you probably want to skip it on index. You would do by putting this at the top of your controller:

skip_before_action :check_xhr, only: [:index]
1 Like

In a plugin you should always create an engine, and then put your custom classes on that. You’ll see this in all our open source plugins.

In your plugin.rb put a block that looks like this and change the names.

module ::DiscourseMachineLearning
  class Engine < ::Rails::Engine
    engine_name "discourse_machine_learning"
    isolate_namespace DiscourseMachineLearning
  end
end

Basically what you’re doing here is creating a seperate ROR environment in which your plugin can live. ROR relies on a lot of namespacing, so this is basically saying: 'this root namespace is mine I will put all my custom stuff here".

For route and controller namespacing, it works like this. First, you write some routes for your special namespace:

DiscourseMachineLearning::Engine.routes.draw do
  get 'index' => "image#index"
end

Then you “mount” it onto the Discourse routes:

Discourse::Application.routes.append do
  mount ::DiscourseMachineLearning::Engine, at: "ml"
end

In this setup

  • The path for “index” is /ml/index. “ml” is the mount point for the plugin’s engine, so it prepends all urls used by the plugin. “index” is the path given when the routes are drawn.

  • The full controller class is DiscourseMachineLearning::ImageController. All routes drawn in an engine have a controller class that starts with the engine module name. In "Image#index", the first part is the controller and the second is the controller “action” or method.

2 Likes

Cool this is super helpful.
I did originally have that code in an Engine, but decided to massively simplify it because I change the way the plugin worked such that it hooked much more into Discourse messages/topics functionality and so the Rails bit got a lot ‘thinner’. I figured it didn’t really need an Engine. But maybe it does.

1 Like

Yeah you should definitely use an engine. Rails uses a lot of namespacing, and you’ll run into issues without one.

2 Likes

In this case the route I needed was

http://baseurl/u/username/things and subpaths like http://baseurl/u/username/things/sent etc

So using Engine namespacing might have made that harder. Is there an Engine way of getting access to a route like that

Yup, so we will still need to use an engine, but we won’t draw the routes on the engine.

Using the Follow plugin as an example

Discourse::Application.routes.append do
  %w{users u}.each_with_index do |root_path, index|
    get "#{root_path}/:username/follow" => "follow/follow#index", constraints: { username: RouteFormat.username }
    get "#{root_path}/:username/follow/following" => "follow/follow#list", constraints: { username: RouteFormat.username }
    get "#{root_path}/:username/follow/followers" => "follow/follow#list", constraints: { username: RouteFormat.username }
  end
end

The key here being follow/follow#index. This translates to

class Follow::FollowController < ApplicationController
  def index
  end
end

Note that the engine for this plugin is

module ::Follow
  class Engine < ::Rails::Engine
    engine_name "follow"
    isolate_namespace Follow
  end
end

So to use your plugin engine for your routes not on the plugin route mount (as they’re user routes), you just need to add the engine to the controller path, i.e. follow/follow

3 Likes

Thanks this is super useful @angus

1 Like

Super important information. Thanks for clarifying the use of engines.

1 Like