2
    $(window).on('unload', function() {
        db.flipCounter.get(gon.slug, function(obj) {
            var payload = {
                slug: gon.slug,
                localFlipCount: obj.fc,
                time: Date.now()
            }

            navigator.sendBeacon('/analytics', csrfProtect(payload))

        })
    })

    function csrfProtect(payload) {
        var param = $("meta[name=csrf-param]").attr("content")
        var token = $("meta[name=csrf-token]").attr("content")

        if (param && token) payload[param] = token
        return new Blob([JSON.stringify(payload)], { type: "application/x-www-form-urlencoded; charset=utf-8" })
    }

In the code above I wish to hit POST to the '/analytics' url with the payload. I get the following error(warning) upon… attempting to fire a request:

Promise.js:840 Unhandled rejection: TypeError: Cannot create property 'authenticity_token' on string '{"slug":"test-page-by-marvin-danig","localFlipCount":1,"time":1524241435403}'

Hm.

…firing a request, I get:

Processing by BooksController#analytics as */*
  Parameters: {"{\"slug\":\"test-book-by-marvin-danig\",\"localFlipCount\":1,\"time\":1524243279653,\"param\":\"8rzDx/TNL8YeU1/NWgWSk6gB/UvmbB9Ip VajDCgfDUv5Q4pjh7x0GUG1il1jDJajtJyHf84Xv5Pt14fiCnA9w"=>"=\"}"}
Can't verify CSRF token authenticity.
exception
ActionController::InvalidAuthenticityToken

UPDATE: The issue isn't resolved still. Here's where I am at right now:

I have the following GET & POST routes open on my routes.rb:

# Analytics (flipCounter)
  get 'auth_token', to: 'analytics#auth_token'
  post 'receptor', to: 'analytics#receptor', as: :receptor  

These obviously map to the analytics_controller like so:

class AnalyticsController < ApplicationController
    respond_to :js

  def auth_token
    session[:_csrf_token] = form_authenticity_token
  end

  def receptor
    logger.debug "Check book slug first: #{params}" 

    begin
      book = Book.friendly.find(params[:slug])
    rescue ActiveRecord::RecordNotFound => e
      book = nil
    end

    if book.exists?
      book.flipcount += params[:flipcount].to_i
    end

  end
    private

end 

Alongside the auth_token method, I got an auth_token.json.erb template that is shipped per following:

{ "authenticity_token": "<%= session[:_csrf_token] %>" } 

And the client side javascript (poor draft) goes the following way:

            // When state of book changes to `not_flipping`:

                flipCount += 1

                const o = { slug: gon.slug, fc: flipCount }

            // IndexedDb initiated elsewhere.
            db.transaction('rw', db.flipCounter, function(e) {
                db.flipCounter.put(o)
            }).then(function(e) {
                const URL = '/auth_token' // First fetch the authenticity_token!
                fetch(URL, {
                    method: 'GET'
                }).then(function(res) {
                    return res.json()
                }).then(function(token) {


                    return postBookData(token)


                }).catch(err => console.log(err))
            }).catch(function(e) {
                console.log(e)
            })


            function postBookData(token) {

                db.flipCounter.get(gon.slug, function(obj) {
                    // var payload = new FormData()

                    // payload.append('slug', gon.slug)
                    // payload.append('localFlipCount', obj.fc)
                    // payload.append('authenticity_token', token.authenticity_token)
                    // payload.append('type', 'application/x-www-form-urlencoded;')
                    // payload.append('charset=utf-8', 'ok')
                    //  payload.append('X-CSRF-Token', token.authenticity_token)

                    //var payload = { 'slug': gon.slug }

                    let body = {
                        slug: gon.slug,
                        flipcount: obj.fc,
                        time: Date.now()
                    }
                    let headers = {
                        type: 'application/x-www-form-urlencoded; charset=utf-8',
                        'X-CSRF-Token': token.authenticity_token
                    }
                    let blob = new Blob([JSON.stringify(body)], headers);
                    let url = '/receptor'

                    navigator.sendBeacon(url, blob);

                }).then(function() {
                    flipCount = 0
                    var o = { slug: gon.slug, fc: flipCount }
                }).catch(err => console.log(err))
            }

The request object fired by the navigator.sendBeacon isn't correct because the X-CSRF-Token isn't set and I obviously get the following error on the server side:

Started POST "/receptor" for 127.0.0.1 at 2018-04-26 09:00:33 -0400
Processing by AnalyticsController#receptor as */*
  Parameters: {"{\"slug\":\"bookiza-documentation-by-marvin-danig\",\"fc\":1}"=>nil}
Can't verify CSRF token authenticity.
exception
ActionController::InvalidAuthenticityToken
  Rendering public/500.html
  Rendered public/500.html (1.0ms)
Completed 500 Internal Server Error in 337ms (Views: 335.8ms | ActiveRecord: 0.0ms)

Has anyone implemented a navigator.sendBeacon scenario on a Rails app over a completely offlined page using service workers?

Marvin Danig
  • 3,738
  • 6
  • 39
  • 71

3 Answers3

7

Just caught this snag, the js end of my solution is as follows:

window.addEventListener("unload", function() {
  var url = "/your_metrics_path",
  data = new FormData(),
  token = $('meta[name="csrf-token"]').attr('content');
  // add your data
  data.append("foo", "bar");
  // add the auth token
  data.append("authenticity_token", token);
  // off she goes
  navigator.sendBeacon(url, data);
});

Hope this helps.

Matt Newell
  • 108
  • 1
  • 5
  • You don't need jquery to get the token, you can use document.querySelector('meta[name="csrf-token"]').content – Kazmin Jan 21 '21 at 18:08
0

You're on the right track, and I've successfully done this, grabbing the CSRF token from the DOM and using it in the JavaScript request. Here is an example of what a normal Rails form is sending in the params:

Started PATCH "/titles/25104" for 127.0.0.1 at 2018-04-20 14:19:11 -0700
Processing by TitlesController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"N97wNps0PMEBcqEsza8gNV741uPZNmltPgJHeeBNmTF0rc2KCaePBlZeCxId+su1sdAYMsgyd/78u9S/mdmprw==" }

It looks like you just need to get things into the right hash structure, and you should be on your way. I think you need to adjust some keys.

Petercopter
  • 1,218
  • 11
  • 16
0

I just learned that it isn't possible to customize the request method, provide custom request headers, or change other processing properties of the request and response when using a Beacon request. See the W3C Editor's Draft for Beacons here.

Use the fetch api instead.

Marvin Danig
  • 3,738
  • 6
  • 39
  • 71
  • 1
    @Matt_Newell's answer is the right one for grabbing the CSRF token from the page and sending it along with the request as form parameters. – Brendon Muir Jul 13 '20 at 01:48