I’m using Rails 6.0.3.4, Devise 4.7.3 for authentication and testing my app on Google Chrome. I have the application containerised using Docker and Docker Compose and I’m using PostgreSQL for the database, ElasticSearch for search and NGINX for the web server.
I can Log In successfully. However when I try to Log Out by clicking on the Log Out button, it doesn’t Log Out and only redirects to the home page.
<%= link_to "Log Out", destroy_user_session_path, method: :delete, :class => 'nav-link' %>
The NGINX server is showing a POST request rather than a DELETE request when trying to Log Out and I’m not sure why this is happening.
37.228.235.151 - - [23/Jan/2021:19:50:12 +0000] "POST /users/sign_out HTTP/1.1" 302 97 "https://myapp.ie/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36" "-"
When I run rake routes, it shows destroy_user_session as:
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
My devise.rb initializer has:
config.sign_out_via = :delete
My Devise controllers are all standard (I haven’t edited them).
Here’s my Application Controller
class ApplicationController < ActionController::Base
protect_from_forgery prepend: true
# To enable sign in to function correctly.
skip_before_action :verify_authenticity_token, :only => :create
before_action :configure_permitted_parameters, if: :devise_controller?
def index
end
protected
# Restrict parameters for sign up input.
def configure_permitted_parameters
added_attrs = [:first_name, :last_name, :email, :encrypted_password, :password_confirmation, :remember_me]
devise_parameter_sanitizer.permit(:sign_up, keys: added_attrs)
devise_parameter_sanitizer.permit(:account_update, keys: added_attrs)
devise_parameter_sanitizer.permit(:sign_in, keys: added_attrs)
end
end
Here’s my Application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>MyApp</title>
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ff8a00">
<meta name="msapplication-navbutton--color" content="#ff8a00">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Text+Me+One&display=swap" rel="stylesheet">
</head>
<body id="<%= controller.controller_name %>" class="<%= controller.action_name %> <%= "show-sidebar" if @show_sidebar.present? %>">
<div class="wrapper">
<% if @show_sidebar.nil? %>
<header>
<%= render "layouts/nav/public" %>
</header>
<%= yield %>
<% else %>
<%= render "layouts/nav/dashboard" %>
<div class="container-fluid">
<div class="row">
<%= render "layouts/nav/sidebar" %>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4 mt-4">
<%= yield %>
</main>
</div>
</div>
<% end %>
<div class="push"></div>
</div>
<%= render "layouts/footer" %>
<div id="notifications"><%= notice %></div>
<!-- PWA Step 1 - Registration -->
<script type="text/javascript">
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/feather-icons/4.9.0/feather.min.js"></script>
<script>feather.replace()</script>
<%= javascript_pack_tag "counter" %>
</body>
</html>
Here’s the part of the view that displays the Log In / Log Out link.
<ul class="navbar-nav mr-auto" id="right-nav-menu">
<li class="nav-item">
<% if notice %>
<p class="nav-link"><%= notice %></p>
<% end %>
<% if alert %>
<p class="nav-link"><%= alert %></p>
<% end %>
</li>
<li class="nav-item">
<% if user_signed_in? %>
<strong class="nav-link"><p><%= current_user.first_name %> <%= current_user.last_name %></p></strong>
<% end %>
</li>
<% if user_signed_in? %>
<li class="nav-item">
<strong><%= link_to 'Edit Profile', edit_user_registration_path, :class => 'nav-link' %></strong>
</li>
<li class="nav-item">
<strong><%= link_to "Log Out", destroy_user_session_path, method: :delete, :class => 'nav-link' %></strong>
</li>
<% else %>
<li class="nav-item">
<strong><%= link_to "Log In", new_user_session_path, :class => 'nav-link' %></strong>
</li>
<li class="nav-item">
<strong><%= link_to "Sign Up", new_user_registration_path, :class => 'nav-link' %></strong>
</li>
<% end %>
</ul>
Appreciate any help.
3
Answers
In your initializer/devise.rb, try to replace delete with get on config.sign_out_via = :delete
I understand that Rails doesn’t do a real DELETE request when using a link_to with
method: :delete
. Check the rails-ujs code here: https://github.com/rails/rails/commit/ad3a47759e67a411f3534309cdd704f12f6930a7#diff-d9f2caa2a91064bb77ada650abdc7085044dffb4e4e35715e8ac64b1c72a0d69R16-R25It creates a form with method POST and a hidden field
_method
. Nginx will see a POST request but rails knows how to read it as a DELETE request.What do you see on the Rails logs? do you get the request log in the console?
TLDR; Don’t worry. Its perfectly fine.
HTML forms can only send GET and POST requests. When you use
link_to ... method: :delete
with Rails UJS or just plain old forms withbutton_to ... method: :delete
a POST request is used. While modern browsers will let you send DELETE and PATCH requests with XHR requests this does not apply to Rails UJS as the data-method handler actually creates a hidden form element and submits it.Rack (the Ruby CGI that Rails is built on top of) gets around this limitation by using a special parameter called
_method
which will make POST requests appear to be DELETE, PUT, PATCH etc. You might have noticed that strange hidden input in the output ofform_for/form_with
. You can also use theHTTP_X_HTTP_METHOD_OVERRIDE
header. See Rack::MethodOverride which is the middleware that transforms the request before it reaches your Rails application.Of course NGINX knows nothing about Rack’s shenanigans and its logs will just show this as regular POST request. The same applies if you use the inspector in your browser to check the traffic.
But if you check the Rails logs you can see by the time the request makes it down the middleware stack to your Rails app Rails will think it was a DELETE request all along.