Fix for Form Post-Submit Redirection Issue with Turbo Drive

With Rails 7 came the addition of Hotwire which contains Turbo. Turbo Drive, which is enabled by default, intercepts link clicks and form submissions. In the case of the latter, some issues may occur if the back end is trying to do a redirect. For instance, let’s suppose we have the following controller code:

class SubscriptionsController < ApplicationController
  def create
    # code...
    redirect_to root_path
  end
end

Then we have the following form:

<%= form_with url: subscriptions_path, method: :post do |f| %>
  <%= # form fields... %>
  <%= f.button 'Subscribe', id: 'submit-button'%>
<% end %>

After submitting the form, we get a “320 found” response because of the redirect the back end is trying to do. Notice we are submitting the form using the POST method. In this case we may encounter some issues like Turbo losing the CSRF token, Caching Issues, etc that may cause some issues like the app not doing the redirect or having some weird partial renderings. It’s worth noting that this issue doesn’t happen if we submit the form using the GET method.

One easy solution to this problem is to disable Turbo using the data-turbo=”false” flag and let the from to be submitted the “old way”:

<%= form_with url: subscriptions_path, data: { turbo: false }, method: :post do |f| %>
  <%= # form fields... %>
  <%= f.button 'Subscribe', id: 'submit-button'%>
<% end %>

InstantClick behavior in Turbo

Today I learned about the new default InstantClick behavior for Turbo. Turbo now prefetches a link when hovering it and changes the page's contents after clicking on it, resulting in instantaneous page changes most of the time. There's a time window of ~300ms between the hover and the click event, so if you want to optimize for this make sure your backend can respond inside that time window.

This is enabled by default but you can opt out of it by adding data-turbo-prefetch="false" to specific links or to whole containers. Add it to your main container to disable it completely.

More info and demos here: https://github.com/hotwired/turbo/pull/1101

An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'

If you're working with APIs like the OpenAI one and you're passing tools to your bots, make sure the results of a tool call are always right next to the tool call message! Otherwise the API will return a 400 response. Heads up! This a recent change in their API (they use to have functions but is now deprecated) and the mandatory order is not mentioned in the official docs!

// It always needs to be like this
[
  { 
    "role": "user", 
    "content": "Tell me the current time", 
    "tools": [{ "name": "current_time", "description": "Gives you the current time" }]
  },
  {
    "role": "assistant",
    "content": "",
    "tool_calls": [{
      "type": "function",
      "id": "d8cdd56d-98e1-4dea-ae3a-2b23",
      "function": { "name": "current_time", "arguments": "{}" }
    }]
  },
  {
    "role": "assistant",
    "content": "Current time is 10:22 AM",
    "tool_call_id": "d8cdd56d-98e1-4dea-ae3a-2b23"
  },
  {
    "role": "system",
    "content": "Just a message after the tool result"
  },
]

Never do this!

[
  { 
    "role": "user", 
    "content": "Tell me the current time", 
    "functions": [{ "name": "current_time", "description": "Gives you the current time" }]
  },
  {
    "role": "assistant",
    "content": "",
    "tool_calls": [{ /* omitted */ }]
  },
  {
    "role": "system",
    "content": "Just a message between the tool call and the tool result"
  },
  {
    "role": "assistant",
    "content": "Current time is 10:22 AM",
    "tool_call_id": "d8cdd56d-98e1-4dea-ae3a-2b23"
  }
]

:use_route in controller specs inside engines

If you have controller specs inside your Rails engine, you'll need to pass use_route: :my_engine when doing the request. Otherwise you'll get an UrlGenerationError since the dummy app is not running and the engine is not mounted!

describe MyEngine::MainController, type: :controller do
  it "does something" do
    get :index, params: { use_route: :my_engine }
    # ...
  end
end

Reduce your slug size on Heroku!

You don't need app/javascript and node_modules after compiling assets. Enhance the assets:precompile task and delete them so they don't get added to your Heroku slug! Small slugs allow for fast boot up times and better scaling. This also helps stay below the 500MB size limit!

Rake::Task["assets:precompile"].enhance do
  next unless Rails.env.production?

  ["#{Dir.pwd}/app/javascript", "#{Dir.pwd}/node_modules"].each do |dir_path|
    FileUtils.rm_rf(dir_path)
  end
end

Mastering extend and when you want class methods and instance methods for your class

Mastering extend and when you want class methods and instance methods for your class

I just realized something cool, which is the underlying of including same module for a class and share instance and class methods with that class.

So here is the trick

You have this simple module:

module TestModule
  def test_name
    'test_name'
  end
end

And this class

class Test
  def name
    'Test'
  end
end
Test.extend(TestModule)
irb(main):033> Test.test_name
=> "test_name"
irb(main):034> Test.new.name
=> "Test"

So that is why if you want to include both instance and class methods, with something like this

class Test
  include TestModule
end

How would you include some as class methods, and the answer is by defining those methods in a module or subgroup within the module, and it would look like this

module TestModule
  def instance_method
    'instance method'
  end

  module TheClassMethods
    def class_hello
      'class_hello'
    end
  end

  def self.included(base)
    base.extend(TheClassMethods)
  end
end

class Test
  include TestModule
end

So now you can access both of them

Test.new.instance_method # instance method
Test.class_hello # class_hello

NOTE: remember that it is included in the past

def self.included(base)
  base.extend(YouClassMethodsToShare)
end

And active_support/concern comparison

module M
  def self.included(klass)
    klass.extend ClassMethods
    klass.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  end

  def instance_method
    'instance method'
  end

  module ClassMethods do
     def class_method_one
       'class method'
     end
  end
end

Vs

require 'active_support/concern'

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }

    def self.direct_class_method_here
      ..
    end
  end

class_methods do
    def class_method_one
      'class method one'
    end
  end
end

class YourClass
  include M
end

Setting up multiple Okta orgs with the same Omniauth Oauth2 Strategy in Rails

Hello There,

I want to share what I did in order to support a second Okta Organization in a Ruby on Rails application using omniauth_oktaoauth gem.

I started with a basic spec to make sure I dont break anything:

RSpec.describe 'Sign in with Okta', type: :request do
  describe "Using Okta" do
    let(:user) { User.find_by email: user_email }
    let(:okta_oauth) do
      {
        provider: okta_provider.to_s,
        uid: "123456789",
        info: {
          name: "John Doe",
          email: user_email,
        }
      }
    end

    before(:each) do
      OmniAuth.config.test_mode = true
      OmniAuth.config.mock_auth[okta_provider] = OmniAuth::AuthHash.new(okta_oauth)
    end

    context "When using existing configuration" do
      let(:okta_provider) { :oktaoauth }
      let(:user_email) { "john.okta@existing.domain" }

      it "keeps working" do
        post "/users/auth/oktaoauth"
        follow_redirect!
        expect(user.email).to eq(user_email)
      end
    end
  end
end

With this, I proceed to write another spec to ensure the new option would work:

context "When using second okta org" do
  let(:okta_provider) { :second_okta }
  let(:user_email) { "john.okta@new.domain" }

  it "signs the right user in" do
    post "/users/auth/second_okta"
    follow_redirect!

    expect(user.email).to eq(user_email)
  end
end

After this, I started to use the normal steps:

# user.rb
devise :omniauthable, omniauth_providers: [:oktaoauth, :second_okta]

Then, modify the initializer to tell omniauth about the new strategy

# config/initializers/okta.rb
....
config.omniauth(:second_okta,
                Rails.configuration.second_okta.client_id,
                Rails.configuration.second_okta.client_secret,
                name: "second_okta",
                request_path: "/users/auth/second_okta",
                callback_path: "/users/auth/second_okta/callback",
                scope: "openid profile email",
                fields: %w[profile email],
                client_options: {
                  site: Rails.configuration.second_okta.url,
                  authorize_url: "#{Rails.configuration.second_okta.auth_issuer}/v1/authorize",
                  token_url: "#{Rails.configuration.second_okta.auth_issuer}/v1/token"
                },
                redirect_uri: "#{Rails.configuration.app.base_domain}/users/auth/second_okta/callback",
                issuer: Rails.configuration.second_okta.auth_issuer,
                strategy_class: OmniAuth::Strategies::Oktaoauth)

At the beginning, it looked easy to add a second option using the same strategy, but after dealing with omniauth internals and omniauth_oktaoauth source code itself, I found that I needed to specify name and strategy_class to override defaults, there's something inside the source code that did not work out of the box, I had to specify request_path and callback_path explicitly (I'll dig deeper later and send a patch if it's a bug).

After making those changes, it worked just fine.

Thanks!

What is the difference between "and" and "&&" in Ruby or between "||" and "or"?

First of all, all of them are logical operators, but the assignment operator = has higher precedence than and and or, but lower precedence than && and ||.

And if not understood correctly it can lead to tricky and unexpected results, for instance:

irb(main):064* def execute_notification
irb(main):065*   puts 'execute_notification'
irb(main):066> end
=> :execute_notification
irb(main):067> execute_notification
execute_notification
=> nil
irb(main):068> result = true and execute_notification
execute_notification
=> nil
irb(main):069> result
=> true
irb(main):070> result = false and execute_notification
=> false
irb(main):071> result
=> false
irb(main):072> result = true or execute_notification
=> true
irb(main):073> result = false or execute_notification
execute_notification
=> nil
irb(main):074> result
=> false
irb(main):075> result = true && execute_notification
execute_notification
=> nil
irb(main):076> result
=> nil
irb(main):077> result = false && execute_notification
=> false
irb(main):078> result
=> false
irb(main):079> result = true || execute_notification
=> true
irb(main):080> result
=> true
irb(main):081> result = false || execute_notification
execute_notification
=> nil
irb(main):082> result
=> nil

Automatically set your Ngrok tunnel as default Rails host for development environment

Ever needed to test your Rails app's emails or background jobs in a local environment but also wanted to expose it through a public URL? Here's a handy trick to dynamically set your default URL options based on Ngrok's current public URL.

Note that you should fire up ngrok before starting your local server. Also this will just grab the first tunnel you have active.

Code: config/initializers/default_url_options.rb

if Rails.env.local?
  ngrok_results = `curl -s -X GET -H "Authorization: Bearer <NGROK_API_KEY>" -H "Ngrok-Version: 2" https://api.ngrok.com/tunnels`
  ngrok_results = JSON.parse(ngrok_results)
  public_url = ngrok_results.dig("tunnels", 0, "public_url")
  host = public_url.gsub("https://", "")
  Rails.application.routes.default_url_options = { host: host }
else
  Rails.application.routes.default_url_options[:host] = "<YOUR PRODUCTION HOST>"
end

How it Works:

  • Checks if the environment is local.
  • Fetches the current Ngrok public URL using the Ngrok API.
  • Parses the JSON response to get the public URL.
  • Updates the default_url_options for the Rails application with this public URL.

Note: Replace <NGROK_API_KEY> and <YOUR PRODUCTION HOST> with your actual keys and host.


Feel free to tweak it!

Exponenciation in Ruby <> Javascript and Ruby to_i in Javascript and prevent rounding float/decimals

Let’s begin with an actual example and conversion for both languages(Ruby and JavaScript).

Most of the time, if you want to take n number of decimals after the . from a big decimal or just fill it with zeros, you would do something like this:

Ruby

number = 4.9999999
# eg: with a precision of 6
'%.6f' % number # => "5.000000"
sprintf('%.6f', number) # => "5.000000"
format('%.6f', number) # => "5.000000"

Javascript

number = 4.9999999
number.toFixed(6) => '5.000000'

How to solve that problem?

By using a custom method!

Ruby

def truncate_float(number, precision)
  factor = 10 ** precision
  (factor * number).to_i / factor.to_f
end

result = truncate_float(4.9, 6)# => 4.9
# then
'%.6f' %  result # => "4.900000"
truncated_value = truncate_float(4.9999999, 6)  # => 4.999999
'%.6f' % truncated_value # => "4.999999"

Now in JavaScript

function truncateDecimal(decimalNumber, precision) {
  factor = Math.pow(10, precision)
  return Math.floor(factor * decimalNumber) / factor
}

truncateDecimal(4.9, 6) // 4.9
truncateDecimal(4.9999999999, 6) // 4.999999

There you have it!

Verify existence of arbitrary email addresses from the command line

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'resolv'
require 'net/smtp'

def mx_records(domain)
  Resolv::DNS.open do |dns|
    dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
  end
end

def mailbox_exist?(email)
  domain = email.split('@').last
  mx = mx_records(domain).first
  return false unless mx

  Net::SMTP.start(mx.exchange.to_s, 25) do |smtp|
    smtp.mailfrom 'info@example.com' # replace with your email address or something more realistic
    smtp.rcptto email
  end
  true
rescue Net::SMTPFatalError, Net::SMTPSyntaxError
  false
end

if ARGV.length != 1
  puts "Usage: ruby #{__FILE__} <email_address>"
  exit 1
end

email = ARGV[0]
if mailbox_exist?(email)
  puts "Mailbox exists."
else
  puts "Mailbox doesn't exist or couldn't be verified."
end