ruby-on-railsparametersfetch

Rails 6, how to render .js.erb with fetch() and params


I am updating old Rails apps that worked with jquery and $ajax(), and this still works after the update. I would like to change the ajax call for a fetch() with .js.erb and params, all seems well, but the html does not get replaced by the workbar_work partial. Any suggestions?

in controller:

def switch_state
  permitted_params = params.permit(:partial)
  @partial = permitted_params[:partial].to_s
  @exercise = current_exercise  # defined somewhere else
  respond_to do |format|
     format.js
  end
end

in switch_state.js.erb

$('#workbar_switch').html("<%= j render(partial: @partial, :locals => {exercise: @exercise@}) %>");

the fetch call

function switchState(partial, exercise){
  const params = new URLSearchParams();
  params.append('partial', partial);

  fetch("/work/switch_state", {
    method: 'POST',
    headers: {
        'Accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript',
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: params
  })
  .then(response =>  {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.text()
  })  
  .then(responseText => {
    console.log("received js", responseText)
  })
  .catch(error => {
    console.log('Update failed', error)
    console.log('error', error.message)
  })
  .finally(() => {
  })
}

rails server log - seems to look ok:

Started POST "/work/switch_state" for ::1 at 2024-12-07 11:19:49 +0100
Processing by WorkController#switch_state as JS
  Parameters: {"partial"=>"workbar_work"}
  Exercise Load (0.3ms)  SELECT "exercises".* FROM "exercises" WHERE "exercises"."unit" = $1 LIMIT $2  [["unit", "ct100_1_ma3"], ["LIMIT", 1]]
  ↳ app/models/exercise.rb:81:in `find_exercise'
  CACHE Exercise Load (0.0ms)  SELECT "exercises".* FROM "exercises" WHERE "exercises"."unit" = $1 LIMIT $2  [["unit", "ct100_1_ma3"], ["LIMIT", 1]]
  ↳ app/models/exercise.rb:81:in `find_exercise'
  Rendering work/switch_state.js.erb
  Rendered work/_workbar_work.html.erb (Duration: 0.6ms | Allocations: 351)
  CACHE Exercise Load (0.0ms)  SELECT "exercises".* FROM "exercises" WHERE "exercises"."unit" = $1 LIMIT $2  [["unit", "ct100_1_ma3"], ["LIMIT", 1]]
  ↳ app/models/exercise.rb:81:in `find_exercise'
  Rendered work/switch_state.js.erb (Duration: 3.4ms | Allocations: 2535)
Completed 200 OK in 17ms (Views: 5.4ms | ActiveRecord: 2.1ms | Allocations: 9998)

browser console:

  [Log] response – Response {type: "basic", url: "http://localhost:3000/work/switch_state", redirected: false, …} (ctrl-dc34a42ad73de8380cf755f815535ed1b8bfc04007d2b4f4fad4a1f130e93d45.js, line 183)
  [Log] received js – "$('#workbar_switch').html(\"<ul>\\n\t\\n\t<a title=\\\"Klicke hier für eine leichte Übung\\\" id=\\\"bug_click\\\" href=\\\"\\\">\\n\t<l…" (ctrl-dc34a42ad73de8380cf755f815535ed1b8bfc04007d2b4f4fad4a1f130e93d45.js, line 190)
  "$('#workbar_switch').html(\"<ul>\\n  \\n <a title=\\\"Klicke hier für eine leichte Übung\\\" id=\\\"bug_click\\\" href=\\\"\\\">\\n  <li id=\\\"workbar_bug\\\">\\n      <i class=\\\"fa fa-bug\\\"><\\/i>\\n        <span id=\\\"bug_text\\\"><\\/span>\\n  <\\/li>\\n<\\/a>    <a title=\\\"andere Stufe\\\" id=\\\"back_to_base_click\\\" href=\\\"\\\">\\n   <li id=\\\"change_level\\\">\\n     <i class=\\\"fa fa-signal\\\"><\\/i>\\n     <span>andere Stufe<\\/span>\\n  <\\/li>\\n<\\/a>    \\n<\\/ul>\\n\\n<form id=\\\"answerForm\\\"\\n        name=\\\"answerForm\\\"\\n      action=\\\"#dummy\\\">\\n\\n  <div id= \\\"answer_field\\\"\\n              title= \\\"Tippe Deine Antwort hier\\\">\\n         <input type = \\\"text\\\"\\n                     id = \\\"txtanswer\\\"\\n                   name = \\\"txtanswer\\\"\\n                     size = \\\"3\\\"\\n                     maxlength = \\\"3\\\" />\\n              <\\/div>\\n  \\n  <button type=\\\"submit\\\" \\n          id=\\\"submit_answer\\\">\\n    <i class=\\\"fa fa-level-down fa-rotate-90\\\"><\\/i>\\n    <span>OK<\\/span>\\n  <\\/button>\\n\\n<\\/form>\\n\\n\");"

Solution

  • The JavaScript returned by the AJAX call is not going to magically execute itself. That's would be really terrible for security.

    When Rails UJS handles AJAX responses it creates a script tag and attaches it to the document which evals the script:

    const script = document.createElement("script");
    script.setAttribute("nonce", cspNonce());
    script.text = response;
    document.head.appendChild(script).parentNode.removeChild(script);
    

    This is basically just an evolution of the JSONP technique used to do cross-domain requests for JSON way back in the dark ages cludged into a (very) poor mans AJAX framework.

    If you want to build on the tower of wonkyness that is js.erb templates it would be way easier to just create a form with JS or have it hidden on the page and trigger a submit to get Rails UJS to handle it for you instead of writing AJAX handlers with fetch.

    Or you could just not wrap your HTML chunk in JavaScript like a sane person.

    def switch_state
      permitted_params = params.permit(:partial) 
      @partial = permitted_params[:partial].to_s # this is super sketchy 
      # a malicous user could use this for code execution or to print things like config files.
      # NEVER accept paths to files from the interwebs.
      @exercise = current_exercise  # defined somewhere else
      render(
        content_type: "text/html"
        partial: @partial, 
        locals: { exercise: @exercise@ },
        layout: false
      )
    end
    
    function switchState(partial, exercise){
      const params = new URLSearchParams();
      params.append('partial', partial);
    
      fetch("/work/switch_state", {
        method: 'POST',
        headers: {
            'Accept': 'text/html',
            'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: params
      })
      // ...
      .then(responseText => {
        $('#workbar_switch').html(responseText);
      })
    }
    

    This is what Turbo does.