Recipes by Category

App Distribution (2) Bundle logic, interface and services for distribution. App Logic (37) The Apex programming language, workflow and formulas for logic. Collaboration (6) The Salesforce Chatter collaboration platform. Database (29) Data persistence, reporting and analytics. Integration (33) Web Service APIs and toolkits for integration. Security (9) Platform, application and data security. Tools (4) Force.com tooling User Interface (36) Visualforce MVC and metadata-drive user interfaces. Web Sites (12) Public web sites and apps with optional user registration and login.
Beta Feedback
Cookbook Home » Interact with the Force.com REST API from Ruby

Interact with the Force.com REST API from Ruby

Post by Pat_Patterson  (2010-10-29)

Status: Unverified
Level: intermediate

Problem

You're writing a Ruby application, using the Sinatra web framework, and you want to call the REST API to create, read, update and/or delete Force.com records.

Solution

Prerequisites

The sample uses the OAuth2 library and the Sinatra web application framework to implement a minimal Ruby web application able to obtain an OAuth 2.0 access token and interact with the Force.com REST API. The full source code is available in the Sinatra-Force.com-Heroku GitHub project.

The Force.com OAuth2 implementation will work with a non-SSL redirect URI for localhost ONLY. This is fine for development, but for production you will need to either configure an SSL reverse proxy in front of the web app, or deploy the code to a cloud platform such as Heroku with an SSL addon. The latter is much easier, but, if you want to configure this run locally, any one of a range of HTTP servers could be used; popular choices include Apache Web Server, lighttpd and nginx. I used nginx, following these instructions to install nginx with SSL support on the Mac and these instructions to configure nginx as an SSL reverse proxy.

You will also need to configure a Remote Access Application - the 'Setup' section of the 'Getting Started with the Force.com REST API' article contains detailed steps. Use https://localhost/oauth/callback as the Callback URL. Note that, if you run the app locally, you must set three environment variables - CLIENT_ID, CLIENT_SECRET and LOGIN_SERVER. The first two are the consumer key and secret from your remote access app; the third will be https://login.salesforce.com for production and developer edition environments, and https://test.salesforce.com for sandboxes.

Full installation instructions are provided in the GitHub project.

Source

This is the complete content of demo.rb

require 'rubygems'
require 'sinatra'
require 'oauth2'
require 'json'
require 'cgi'
require 'dalli'
require 'rack/session/dalli' # For Rack sessions in Dalli

$stdout.sync = true

# Dalli is a Ruby client for memcache
def dalli_client
  Dalli::Client.new(nil, :compression => true, :namespace => 'rack.session', :expires_in => 3600)
end

enable :sessions

# Use the Dalli Rack session implementation
use Rack::Session::Dalli, :cache => dalli_client

# Set up the OAuth2 client
def oauth2_client
  OAuth2::Client.new(
    ENV['CLIENT_ID'],
    ENV['CLIENT_SECRET'], 
    :site => ENV['LOGIN_SERVER'], 
    :authorize_url =>'/services/oauth2/authorize', 
    :token_url => '/services/oauth2/token',
    :raise_errors => false
  )
end

# Subclass OAuth2::AccessToken so we can do auto-refresh
class ForceToken < OAuth2::AccessToken
  def request(verb, path, opts={}, &block)
    response = super(verb, path, opts, &block)
    if response.status == 401 && refresh_token
      puts "Refreshing access token"
      @token = refresh!.token
      response = super(verb, path, opts, &block)
    end
    response
  end
end

# Filter for all paths except /oauth/callback
before do
  pass if request.path_info == '/oauth/callback'
  
  token         = session['access_token']
  refresh       = session['refresh_token']
  @instance_url = session['instance_url']
  
  if token
    @access_token = ForceToken.from_hash(oauth2_client, { :access_token => token, :refresh_token =>  refresh, :header_format => 'OAuth %s' } )
  else
    redirect oauth2_client.auth_code.authorize_url(:redirect_uri => "https://#{request.host}/oauth/callback")
  end  
end

after do
  # Token may have refreshed!
  if @access_token && session['access_token'] != @access_token.token
    puts "Putting refreshed access token in session"
    session['access_token'] = @access_token.token
  end
end

get '/oauth/callback' do
  begin
    access_token = oauth2_client.auth_code.get_token(params[:code], 
      :redirect_uri => "https://#{request.host}/oauth/callback")

    session['access_token']  = access_token.token
    session['refresh_token'] = access_token.refresh_token
    session['instance_url']  = access_token.params['instance_url']
    
    redirect '/'
  rescue => exception
    output = '<html><body><tt>'
    output += "Exception: #{exception.message}<br/>"+exception.backtrace.join('<br/>')
    output += '<tt></body></html>'
  end
end

get '/' do
  # Field list isn't very volatile - stash it in the session
  if !session['field_list']
    session['field_list'] = @access_token.get("#{@instance_url}/services/data/v21.0/sobjects/Account/describe/").parsed
  end
  
  @field_list = session['field_list']
  
  if params[:value]
    query = "SELECT Name, Id FROM Account WHERE #{params[:field]} LIKE '#{params[:value]}%' ORDER BY Name LIMIT 20"
  else
    query = "SELECT Name, Id from Account ORDER BY Name LIMIT 20"
  end
  
  @accounts = @access_token.get("#{@instance_url}/services/data/v20.0/query/?q=#{CGI::escape(query)}").parsed
  
  erb :index
end

get '/detail' do
  @account = @access_token.get("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}").parsed
  
  erb :detail
end

post '/action' do
  if params[:new]
    @action_name = 'create'
    @action_value = 'Create'
    
    @account = Hash.new
    @account['Id'] = ''
    @account['Name'] = ''
    @account['Industry'] = ''
    @account['TickerSymbol'] = ''

    done = :edit
  elsif params[:edit]
    @account = @access_token.get("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}").parsed
    @action_name = 'update'
    @action_value = 'Update'

    done = :edit
  elsif params[:delete]
    @access_token.delete("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}")
    @action_value = 'Deleted'
    
    @result = Hash.new
    @result['id'] = params[:id]

    done = :done
  end  
  
  erb done
end

post '/account' do
  if params[:create]
    body = {"Name"   => params[:Name], 
      "Industry"     => params[:Industry], 
      "TickerSymbol" => params[:TickerSymbol]}.to_json

    @result = @access_token.post("#{@instance_url}/services/data/v20.0/sobjects/Account/", 
      {:body => body, 
       :headers => {'Content-type' => 'application/json'}}).parsed
    @action_value = 'Created'
  elsif params[:update]
    body = {"Name"   => params[:Name], 
      "Industry"     => params[:Industry], 
      "TickerSymbol" => params[:TickerSymbol]}.to_json

    # No response for an update
    @access_token.post("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}?_HttpMethod=PATCH", 
      {:body => body, 
       :headers => {'Content-type' => 'application/json'}})
    @action_value = 'Updated'
    
    @result = Hash.new
    @result['id'] = params[:id]
  end  
  
  erb :done
end

get '/logout' do
  # First kill the access token
  # (Strictly speaking, we could just do a plain GET on the revoke URL, but
  # then we'd need to pull in Net::HTTP or somesuch)
  @access_token.get(ENV['LOGIN_SERVER']+'/services/oauth2/revoke?token='+session['access_token'])
  # Now save the logout_url
  @logout_url = session['instance_url']+'/secur/logout.jsp'
  # Clean up the session
  session['access_token'] = nil
  session['instance_url'] = nil
  session['field_list'] = nil
  # Now give the user some feedback, loading the logout page into an iframe...
  erb :logout
end

get '/revoke' do
  # For testing - revoke the token, but leave it in place, so we can test refresh
  @access_token.get(ENV['LOGIN_SERVER']+'/services/oauth2/revoke?token='+session['access_token'])
  puts "Revoked token #{@access_token.token}"
  "Revoked token #{@access_token.token}"
end

Run the sample with

ruby demo.rb
or follow the instructions to deploy it on Heroku.

Restart your browser to clear out any session cookies, then browse to the app's URL (http://localhost/, or your Heroku app's endpoint - don't forget, it must be https!) and click the link. Login as usual, and you will be presented with a screen requesting authorization for the sample app to access your data. On approving access, you will see the app's output in the browser, similar to the following:

You can create a new Account record, or click an account name to go to a minimal Account detail page. From the detail page you can delete or edit the Account record.

Discussion

The before filter checks whether there is an OAuth access token in the session. If there is, then it creates an OAuth2::AccessToken object from the stored data, otherwise, it renders the auth.rb view, which checks if the browser connected via https. If the connection is secure, the browser is redirected to authenticate at salesforce.com, otherwise an error message is shown.

On authenticating the user and obtaining the user’s consent for the app to access the user’s data, Salesforce redirects the browser back to /oauth/callback, where the handler extracts the code query parameter and uses the OAuth2 library to obtain an access token. The access token and instance URL are saved to the session, and the browser is redirected to /.

Note, near the top of demo.rb:

# Dalli is a Ruby client for memcache
def dalli_client
  Dalli::Client.new(nil, :compression => true, :namespace => 'rack.session', :expires_in => 3600)
end

# Use the Dalli Rack session implementation
use Rack::Session::Dalli, :cache => dalli_client

This code creates a Dalli client with which to interact with memcache, and sets it as the Rack session handler. In contrast to Rack’s default cookie-based sessions, sessions in memcache are independent of the Ruby server process, can be load-balanced across server instances, and survive restart of the server.

The / handler calls describe on the Account object, and caches the result in the session, since this is relatively static data. Next, it retrieves a list of accounts from Salesforce via the access token’s get method, and renders a page via erb. The index page shows a dropdown list of fields on which the user can search, and a list of 20 accounts that match the search parameters.

The handler for /detail simply retrieves a single Account record and renders a subset of its data via detail.erb. The /action and /account handlers render pages for creating and updating records, and apply those actions to the Account record respectively. The OAuth2::AccessToken methods make it very easy to manipulate records via the REST API.

Access tokens periodically expire, so the ForceToken class wraps OAuth2::AccessToken's request method to test for a 401 'unauthorized' response, refreshing the token and retrying the request. The /revoke endpoint allows you to easily simulate a token expiring, and test that the application does correctly refresh the token, providing uninterrupted service.

Finally, the /logout handler revokes the access token, cleans up the session, and logs the user out via the browser. Note the use of an invisible iframe in logout.erb to terminate the browser session.

Since the Ruby libraries do not yet support the PATCH HTTP method, in updateAccount we use the POST method, overriding it via the _HttpMethod query string parameter.

References

Share

Recipe Activity - Please Log in to write a comment

Thanks for the detailed explanation.

When I do ruby demo.rb, Nothing happens

$ ruby demo.rb 
$

Can you please provide pointers to resolve this issue?

by a093000000YEN4v  (2014-02-27)

when i am running above apllication.It is throwing below exception.
Please help me in this reard.

Exception: No server available

/app/vendor/bundle/ruby/2.0.0/gems/dalli-1.1.4/lib/dalli/ring.rb:45:in `server_for_key'

by venkat reddy  (2014-02-09)

Exception: No server available

Solution - start memcached

by a093000000VEvGE  (2012-10-02)

I have this installed on localhost, but get the same error as Andrew Taylor reported on 2012-05-15.  Is there a known solution?

Exception: No server available
/.rvm/gems/ruby-1.9.3-p194/gems/dalli-1.1.4/lib/dalli/ring.rb:45:in `server_for_key'
...
/.rvm/gems/ruby-1.9.3-p194/gems/rack-1.4.1/lib/rack/session/abstract/id.rb:71:in `[]='

demo.rb:74:in `block in '

by a093000000VEvGE  (2012-10-02)

I installed this on heroku, but am getting the following error:

Exception: No server available

/app/.bundle/gems/ruby/1.9.1/gems/dalli-1.1.4/lib/dalli/ring.rb:45:in `server_for_key'

Anyone else encounter this?

by Andrew Taylor  (2012-05-15)

Pat - Yes, got it working few months back. On Heroku it works. 

by Varun Kaushish  (2012-02-16)

Varun - it looks like your certificate store does not include the correct root certs. I would recommend deploying on Heroku - everything works just great there.

by Pat_Patterson  (2012-02-09)

 The Asf-REST-Adapter is now available. It is @ version 0.1.

You can download from RubyGems   -> asf-rest-adapter
or You can see the project -> http://github.com/raygao/asf-rest-adapter

by Raymond Gao  (2011-03-08)

def showAccounts access_token
        response = JSON.parse(access_token.get("#{INSTANCE_URL}/services/data/v20.0/query/?q=#{CGI::escape('SELECT Name, Id from Account LIMIT 100')}"))
         
        var  = ''
        response['records'].each do |record|
        var  += "#{record['Id']}, #{record['Name']}<br/>"
 
        end
        var  += '<br/>'
 
    end

by Varun Kaushish  (2011-02-17)

def callback
    
    access_token = client.web_server.get_access_token(params[:code], :redirect_uri => "https://localhost/flight/oauth/callback", :grant_type => 'authorization_code')
    @print = access_token.token
*************************************************
Till this point code works good. I'm able to publis @print value with .token. But the moment i invoke showAccounts. I start getting
Receieved HTTP 403 During request.
**************************************************
    showAccounts access_token
 
 
    
  end

by Varun Kaushish  (2011-02-17)

 when I go to https://localhost:4567,   I get the following error in my console:

Wed Feb 16 10:37:05 -0600 2011: HTTP parse error, malformed request (127.0.0.1): #<Mongrel::HttpParserError: Invalid HTTP format, parsing fails.>
Wed Feb 16 10:37:05 -0600 2011: REQUEST DATA: "\026\003\000\000Q\001\000\000M\003\000M[˝1∑ˇû£Yˆô
\265\263\257\256\002}\205B\003ÃGËëU\246M…\023\000\000&\000/\000\005\000\004\0005\000\n\000\t\000\003\000\b\000\006\0002\0003\0008\0009\000\026\000\025\000\024\000\023\000\022\000\021\001\000"
---
PARAMS: {}
---
 

by Raymond Gao  (2011-02-16)

Good writing!
But, when I point my browser to https://localhost:4567, I am getting unable to connect. What might be causing that?

Thanks,

by Raymond Gao  (2011-02-16)

Hi Varun, Can you put some code in to dump the content of the response - usually that gives more of a clue as to the problem.

by Pat_Patterson  (2011-02-08)

 Able to resolve the issue using Faraday adapter monkey patch, by setting http.verify_mode = OpenSSL::SSL::VERIFY_NONE;
But i have new problem now. I'm getting following error

OAuth2::HTTPError in OauthController#callback
Received HTTP 400 during request.
Thanks

by Varun Kaushish  (2011-01-24)

 Hi Pat
 Great write !! :) Thanks. I'm facing some issue while getting response back from SF. 
Error message::


SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
Can you point me to possible solution. I'm using R3, Apache 2.2 as reverse proxy, OpenSSL on window 7 machine.
Thanks


by Varun Kaushish  (2011-01-24)

X

Vote to Verify a Recipe

Verifying a recipe is a way to give feedback to others and broaden your own understanding of the capabilities on Force.com. When you verify a recipe, please make sure the code runs, and the functionality solves the articulated problem as expected.

Please make sure:
  • All the necessary pieces are mentioned
  • You have tested the recipe in practice
  • Have sent any suggestions for improvements to the author

Please Log in to verify a recipe

You have voted to verify this recipe.