スマホAPI の Controller 実装でよくつかう HTTPステータスコードの意図別まとめ
はじめに
Rails でスマートフォン向けの API をつくっている時にどの HTTPステータスコードを返すか、自分なりの判断基準をまとめてみました。
Controller を実装している時によく返すステータスコードをイメージしています。
200 :ok
- 以下が正常に行われた場合
- GET系。action: index, show
- UPDATE系。action: update
- DELETE系。action: destroy
201 :created
- 以下が正常に行われた場合
- POST系。action: create
400 :bad_request (2015/04/15 更新)
- 必要なパラメータが存在しない場合
- 例: 緯度と経度はセットでほしい
- パラメータが範囲を超えている場合
- 例: 検索条件のパラメータの範囲がオーバーしている
- クライアントを普通に使っていれば起こりえないリクエストを受け取ったとき
401 :unauthorized
403 :forbidden (2015/08/06 更新)
- ユーザが退会や BANされている状態で認証を試みた場合
404 :not_found
409 :conflict (2015/08/06 更新)
- リソースに矛盾がおきそうな場合
422 :unprocessable_entity
- バリデーションエラーの場合
- http://stackoverflow.com/questions/15225325/is-http-status-422-appropriate-for-records-that-fail-uniqueness-validation
- http://tools.ietf.org/html/rfc4918#section-11.2
- "the syntax of the request entity is correct (snip) but was unable to process the contained instructions"
さいごに
だいたいのケースは上記で間に合っている気がしてます。
他にもこういうステータスコード使うよ、このステータスコードはこういうシーンでも使ってるよ、それを表現するにはもっといいステータスコードがあるよ、などありましたら教えてください :)
参考
参考: Restful Webサービス
おまけ: ステータスコードの数字と意味/シンボルの見方
rails console> pp Rack::Utils::HTTP_STATUS_CODES => {100=>"Continue", 101=>"Switching Protocols", 102=>"Processing", 200=>"OK", 201=>"Created", 202=>"Accepted", ... 511=>"Network Authentication Required"}
rails console> pp Rack::Utils::SYMBOL_TO_STATUS_CODE => {:continue=>100, :switching_protocols=>101, :processing=>102, :ok=>200, :created=>201, :accepted=>202, ... :network_authentication_required=>511}
コードに数字が書いてあると意味の変換に脳内で時間がかかるので Symbol を使うのが好みです。
Rails で iOS のプッシュ通知を実装する手順
(最終更新日: 2017/08/23)
はじめに
自前でプッシュ通知を実装する手順をまとめます。サーバからクライアントまで。
この記事に書いてあること:
- プッシュ通知の疎通確認までの手順
- 個別送信の実装例
- 複数送信の実装例
- デプロイ時の工夫
- Feedback のハンドリング
前提
- クライアント
- 対応OS
- iOS7, iOS8
- サーバ
- 単一サービスのプッシュ通知を実装する
- 複数サービスになると rpush はスレッド数が比例して増大するので向いてないことがある
証明書/プロビジョニングを用意
Developer Center から以下のファイルを取得します。
- プッシュ通知用の証明書
- プロビジョニング
- Provisioning Profiles から適宜選択
- 証明書を変えたりプッシュ通知を enable にしたら作りなおして登録するのを忘れずに
プロビジョニングと APNS環境の対応は以下のとおりです。必要に応じて準備してください。
- development のプロビジョニングでビルドすると APNS環境は sandbox となる
- distribution (adhoc/appstore) のプロビジョニングでビルドすると APNS環境は production となる
異なる APNS環境のデバイストークンを DB に混在させないように注意しましょう。
クライアントを実装
プッシュ通知を送るにはデバイストークンを発行する必要があります。デバイストークンはプッシュ通知を送る時の識別子です。
ためしに起動時に発行してみましょう。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self registerPushNotificationToken]; // 省略 } - (void)registerPushNotificationToken { UIApplication *sharedApplication = [UIApplication sharedApplication]; if ([sharedApplication respondsToSelector:@selector(registerUserNotificationSettings:)]) { // iOS 8 UIUserNotificationSettings *userNotificationSettings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound categories:nil]; [sharedApplication registerUserNotificationSettings:userNotificationSettings]; [sharedApplication registerForRemoteNotifications]; } else { // iOS 7 [sharedApplication registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]; } } /* デバイストークン発行成功 */ - (void)application:(UIApplication*)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { NSLog(@"deviceToken: %@", deviceToken); NSString *deviceTokenString = [[[[deviceToken description] stringByReplacingOccurrencesOfString:@"<" withString:@""] stringByReplacingOccurrencesOfString:@">" withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@""]; // サーバにデバイストークンを送信 // // ローカルに前回のデバイストークンを保存してあるなら、比較して同じだったら送信する必要はない。余計なリクエストはしない } /* デバイストークン発行失敗 */ - (void)application:(UIApplication*)app didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { // ハンドルが必要なら処理を書く }
デバイストークンはバックアップをインストールしたときやOSの再インストール時などに変わることがあります。一度送信したらその後送信しない、と実装すると変わった拍子にプッシュ通知が届かなくなってしまいます。デバイストークンが変わったら適宜サーバへ送りましょう。
参考: Is the device token as unique as the device ID?
デバイストークンは実機のみで発行できます。シミュレータだとデバイストークンを発行できないので注意しましょう。ログを出すなどしてデバイストークンが発行できていることを確認してください。
サーバに鍵を持たせる
プッシュ通知を送るためにはサーバが正しい送信元だと証明しなければなりません。そのために先ほど証明書から鍵を書き出してサーバに配置する必要があります。
以下の手順で鍵を書き出します。
- キーチェーンアクセスを起動します
- 分類> 自分の証明書 > “Apple <Development|Production> IOS Push Services:
” の証明書を選択 - 右クリックして書き出すを選択し、p12 として保存します
- このとき「種類」が「証明書」のところで書き出しをします。それにより証明書と秘密鍵を含めたものが書き出されます
- 「種類」が「秘密鍵」のところで書き出すとプッシュ通知が飛ばないので注意しましょう
- また「種類」の「秘密鍵」が存在することも確認しましょう。存在しないとプッシュ通知は飛びません。p12 を別ファイルとして持っているならダブルクリックすると入ります
保存した p12 を pem に変換します <CERT_NAME> は適宜うめてください。
$ openssl pkcs12 -in <CERT_NAME>.p12 -out <CERT_NAME>.pem -nodes -clcerts
pem を $RAILS_ROOT/certificates/apns
配下に [sandbox|production].pem
という名前をつけて配置します。
サーバでデバイストークンを受け取る
トークンを受け取る API を用意しましょう。User モデルが device_token アトリビュートを持つとします。
config/routes.rb
... namespace :api, defaults: {format: 'json'} do namespace :v1 do namespace :users do resource :device_token, only: [:update] end end end ...
app/controllers/api/v1/users/device_tokens_controller.rb
class Api::V1::Users::DeviceTokensController < Api::V1::ApplicationController # @param [String] device_token def update current_user.device_token = params[:device_token] ActiveRecord::Base.transaction do current_user.save! current_user.sweep_same_device_tokens end render status: :ok end end
app/models/user.rb
class User < ActiveRecord::Base # 自分以外の同じデバイストークンを持つユーザがいたら nil で上書き # 一つのデバイストークンは複数のユーザで持たないべき def sweep_same_device_tokens(updated_time: Time.zone.now) User .where(device_token: self.device_token) .where.not(id: self.id) .update_all(['device_token = NULL, updated_at = ?', updated_time]) end end
イメージはこんな感じです。sweep の処理があるので API の口は専用で用意したほうがいいと考えています。
さて、クライアントからデバイストークンを受け取りましょう。受け取れたら次のステップです。
まずは開発コンソールからプッシュ通知を送って疎通を確認
rpush という gem を使います。
Gemfile に追加し $ bundle install
し $ rpush init
します。
config/initializer/rpush.rb を適宜設定します。
lib/tasks/rpush.rake に環境をロードするスクリプトを配置します。
require_relative '../../config/environment' namespace :rpush do # USAGE: # () 内は省略可 # $ bundle exec rake rpush:install (APNS_ENV=[sandbox|production]) task install: :environment do |current_task| puts "start #{current_task.name}" app_name = 'myproject' apns_env = ENV['APNS_ENV'] ? ENV['APNS_ENV'] : (Rails.env.production? ? 'production' : 'sandbox') if Rpush::Apns::App.where(name: app_name, environment: apns_env).exists? puts 'already exists: (Rpush::Apns::App (name:%s environment:%s))' % [app_name, apns_env] next # end the task (almost same as method's return) end certificate_path = Rails.root.join('certificates', 'apns', "#{apns_env}.pem") Rpush::Apns::App.create!( name: app_name, certificate: File.read(certificate_path), environment: apns_env, password: '', connections: 1 ) puts "saved: RAILS_ENV=#{Rails.env}, APNS_ENV=#{apns_env}, certificate_path=#{certificate_path}" puts "end #{current_task.name}" end end
$ bundle exec rake rpush:install
そしてプッシュ通知を送るためのプロセスも立ち上げます。
$ bundle exec rpush start
ようやく準備が整いました。プッシュ通知を送ってみましょう。
irb> n = Rpush::Apns::Notification.new irb> n.alert = 'hello apns!' irb> ... irb> n.save!
これでクライアントに ‘hello apns!’ という文言でプッシュ通知が届けば OK です!
個別にプッシュ通知を送る実装
送信するメソッドを定義します。
# モデルを分けるなり、親controller に持たせるなり def send_notification(device_token:, i18n_key:) return nil unless device_token return nil unless i18n_key n = Rpush::Apns::Notification.new n.app = Rpush::Apns::App.find_by_name('myproject') n.device_token = device_token n.alert = I18n.t(i18n_key) n.save! end
送信したいところで
send_notification(device_token: current_user.device_token, i18n_key: 'errors.notifications.got_message')
と呼びます。
複数にプッシュ通知を送る通知の実装例
ユーザ全員に送るお知らせプッシュ通知などを想定しています。
実装の雰囲気こんな感じです。rake task や管理画面などから送ることが多い印象です。
message = 'プッシュ通知で送りたい文字列。' sent_users_count = 0 # プッシュ通知を送れたユーザ数。端数は無視していることに注意 batch_size = Settings.apns.batch_size # 100 程度がいいかも。一つの通知に失敗するとすべて fail することに注意 notificatees = User.where('device_token IS NOT NULL') logger = ActiveSupport::BufferedLogger.new(Rails.root.join('log', 'users_notification.log')) APNS.host = Settings.apns.host APNS.pem = Settings.apns.pem logger.info "\n\n\n\n[#{Time.now}] START Push Notification, number of receivers will be #{notificatees.count}. Message: #{message}" logger.info "[#{Time.now}] notificatees.to_sql: #{notificatees.to_sql}" notificatees.find_in_batches(batch_size: batch_size) do |batched_notificatees| retry_count = 0 time = Time.now notifications = batched_notificatees.map {|notificatee| APNS::Notification.new(notificatee.device_token, alert: message, badge: 1, sound: 'default') } # 通知の送信 # APNS の調子次第で Errno::EPIPE が稀に発生するのでリトライ # TODO retryable gem でもっと綺麗に書けそう begin APNS.send_notifications(notifications) rescue Errno::EPIPE => e logger.error "[#{time}] Failed(##{retry_count + 1}) and retry #{e.class}, #{e.message}\n#{e.backtrace.join("\n")}" retry_count += 1 retry if retry_count < 3 logger.error "[#{time}] *** RETRY FAILED ***" rescue => e logger.error "#{e.class}, #{e.message}\n#{e.backtrace.join("\n")}" end sent_users_count += batch_size logger.info "[#{time}] Push was sent to about #{sent_users_count} users." end
デプロイ時の工夫
capistrano をこんなふうにしておくとラクです。環境をロードしたりプロセスを立ち上げたりしてくれます。
lib/capistrano/tasks/rpush.rake
namespace :rpush do task :environment do set :rpush_pid, "#{shared_path}/tmp/pids/rpush.pid" end def start_rpush within current_path do execute :bundle, :exec, :rpush, :start, "-e #{fetch(:rails_env)}" end end def stop_rpush execute :kill, "-s QUIT $(< #{fetch(:rpush_pid)})" # XXX: kill時に pidファイルが消えないので消す。もっといい方法あれば知りたい # たとえば unicorn gem のプロセスは、 # kill時にプロセスが pidファイルを消してくれるからこういうのを書かなくていい execute "rm #{fetch(:rpush_pid)}" end def reload_rpush execute :kill, "-s HUP $(< #{fetch(:rpush_pid)})" end def rpush_process_exists? test("[ -f #{fetch(:rpush_pid)} ]") end desc 'Install rpush settings to DB' task :install => :environment do on roles(:app) do within release_path do with rails_env: fetch(:rails_env) do execute :rake, 'rpush:install' end end end end desc 'Start rpush server' task :start => :environment do on roles(:app) do start_rpush end end desc 'Stop rpush server' task :stop => :environment do on roles(:app) do stop_rpush end end desc 'Restart rpush server' task :restart => :environment do on roles(:app) do if rpush_process_exists? reload_rpush else start_rpush end end end end
config/deploy.rb
after :migrate, 'rpush:install' namespace :deploy do after :restart, 'rpush:restart' end
Feedback を受け取って無効なデバイストークンを掃除する
ユーザがアプリを消すなどしてプッシュ通知がとどかないデバイストークンがある。そういったデバイストークンに通知を送るのは帯域がムダ。APNS は通知できなかったデバイストークンを Feedback サーバに保持している。production 環境から定期的に Feedback へ問い合わせて無効なデバイストークンを削除しましょう。
一括で掃除できるメソッドをどこかしらのモデルに定義しておきます。
class << self def sweep_invaild_device_tokens(tokens:) where(device_token: tokens).each do |user| user.update(device_token: nil) end end end
それを rpush の initializer で呼びます。ポーリングの秒数も設定しましょう。
config/initializers/rpush.rb
Rpush.configure do |config| # Frequency in seconds to check for feedback config.feedback_poll = 60 ... end Rpush.reflect do |on| on.apns_feedback do |feedback| logger = Logger.new(Rails.root.join('log', 'rpush_feedback.log')) logger.formatter = Logger::Formatter.new tokens = [feedback.device_token].flatten.compact logger.info('Start sweeping %d invalid device tokens' % tokens.count) if tokens.present? tokens.each do |token| logger.info(token) end User.sweep_invaild_device_tokens(tokens: tokens) end logger.info 'End sweeping invalid device tokens' end ... end
メモ
- 動作確認は production で確認するしかない? sandbox だと on.apns_feedback が kick されていないような気がする。TODO もうちょっと調べる。
- 100 件をまとめて送信する際に、1 件でも無効なトークンが含まれていると、100 件とも送信失敗になったような気がする。要確認。
環境まわりの考察
ちょっとモヤモヤしていることをダンプしておきます。
- PROVISIONING=development では IPA を抽出できない。つまり deploygate で配布できない。端末をつないでインストールしかできない。開発時にリモートの人に確認してもらいたい時に困る
- staging では APNS_ENV=production トークンを使って feedback を受け取らないように rpush を改造。ゴリ押し感
- rpush で feedback を取るときに取り合いになる。これは gem の設定で off にするか改造するか pull req すれば解決する
- 2015/2/18 に feedback_receiver.enabled オプションが追加されたので改造しなくて大丈夫
- 申請前なら APNS_ENV=production で確認してもらえないこともないが、それ以降はできない方法
- RAILS_ENV=staging に APNS_ENV=production のトークンを混ぜたくない理由 (悩ましい・・)
- 本番じゃないのに本番じゃないトークンはいれたくない
- ないとは思うが RAILS_ENV=staging から APNS_ENV=production への誤送信が怖い
理想:
- RAILS_ENV=development,staging, PROVISIONING=development では PUSH_ENV=sandbox
- RAILS_ENV=production, PROVISIONING=distribution (adhoc/appstore) では PUSH_ENV=production
参考
Rails サーバから Google Analytics API で情報を取得する手順 ーー google-api-ruby-client, OAuth
はじめに
Rails サーバから Google Analytics API をたたいて情報を取得する手順をまとめます。
google/google-api-ruby-client · GitHub を使います。ログインだと Web で認可をもとめられる(後述)ので OAuth を使います。
日にちごとのスクリーンビューを取得するスパイクをつくるのがゴールです。
OAuth に必要な鍵を修得
- Google Analytics で使用するアカウントで Google Developers Console にアクセス
- プロジェクトを作成
- 作成したプロジェクトをクリック
- APIs & Auth > Credentials を選択
- Client ID を新規作成。Service account を選択
- p12 をダウンロード
ライブラリをインストール
Gemfile に追加します。
gem 'google-api-client' # Google Analytics API から情報を取得するためのクライアント gem 'signet' # OAuth 認証
インストールします。
$ bundle install
クライアントのモデルを用意
app/models/ga_client.rb を新規作成します。"EDIT" コメントの部分をてきぎ書き換えてください。
require 'google/api_client' # Google Analytics Client class GaClient def initialize( application_name: 'My App Google Analytics', # EDIT application_version: '1.0.0' # EDIT ) @client = Google::APIClient.new( application_name: application_name, application_version: application_version ) end def api @api ||= @client.discovered_api('analytics', 'v3') end def signing_key return if @signing_key keyfile = Rails.root.join('certificates', 'google_analytics.p12') # see: http://stackoverflow.com/questions/14525565/not-sufficient-permissions-google-analytics-api-service-account # see: http://stackoverflow.com/questions/11534438/what-is-the-google-api-password-for-the-oauth-pkcs-p12-private-key passphrase = 'notasecret' @signing_key = Google::APIClient::KeyUtils.load_from_pkcs12(keyfile, passphrase) end def authorize! @client.authorization = Signet::OAuth2::Client.new( token_credential_uri: 'https://accounts.google.com/o/oauth2/token', audience: 'https://accounts.google.com/o/oauth2/token', scope: 'https://www.googleapis.com/auth/analytics.readonly', issuer: 'xxxx@developer.gserviceaccount.com', # EDIT signing_key: signing_key ) @client.authorization.fetch_access_token! end def daily_screen_view(date:, screen_name:) # 管理画面の Admin > VIEW > View Settings に記載されている View ID を指定 ga_id = 'ga:XXX' # EDIT result = @client.execute( api_method: api.data.ga.get, parameters: { ids: ga_id, 'start-date' => date.to_s, 'end-date' => date.to_s, metrics: 'ga:screenviews', filters: "ga:screenName==#{screen_name}", dimensions: 'ga:date', } ) body = JSON.parse(result.response.body) date, screen_view = body['rows'].first screen_view end end
クライアントを使って情報を取得
呼び出し側を実装します。「マイページ」というスクリーン名のスクリーンビューを取得します。
client = GaClient.new client.authorize! puts client.daily_screen_view(date: Date.today, screen_name: 'マイページ') #=> 1024
今日のスクリーンビューは 1024 だとわかりました!
他の値もとりたい
以下を参照してください。
参考文献
- Google Analytics API における OAuth の全体像
- OAuth の playground: OAuth 2.0 Playground
紆余曲折
情報を取得するまでにいろいろと試行錯誤しました。そのログを残します。
- Sija/garb · GitHub を見つける
- ログイン(メールアドレスとパスワードで認証)でやってみよう
- ローカルで動いた。サーバにデプロイして確認してみよう。おや……動かないぞ……
- "Error=BadAuthentication Info=WebLoginRequired" とエラーを吐いていた。Webログインが必要……!?
- 普段つかわない端末からアクセスしたら Google が不審なアクティビティとしてはじくようだ
- 端末から https://accounts.google.com/displayunlockcaptcha にアクセスして認可すればいいらしい
- とはいえサーバから Webブラウザは立ちあげられない
- いろいろ試す
- Activity 一覧 から認可できないかなと思ったがムリそうだった
- こんなふうに curl で叩いて どうにかならないかと思ったがどうにかならなかった
- ひとまずログインでなく OAuth でやってみよう。長時間かかっても解決しなさそうだったらサーバに proxy を立てて Web認証かな……(やりたくない)
- garb だと OAuth つかうのしんどそうだった。Web を一度介さないとアクセストークンを取得するのができなさそう
- 他にやりかたないかな。omniauth はしんどそうだなあ
- PHP にはクライアントあるのか。Ruby にもないかな
- あった!
記事のはじめに戻ります。