2013/06/08

【C#でTwitter】タイムライン取得で多発するエラーに挫折しそう(API1.1)

Twitterクライアントを作っているときに出会った数々のエラーとその対応法です。

ただ、私自身、401:Unauthorized で完全にハマり丸3日間悩んでいました。
解決した部分だけでも役に立てばと思いまとめます。 


このエントリを読まれた方で対策がわかる方はぜひ教えてください!

(このエントリを書いた翌日にタイムライン取得ができました!
タイムライン取得方法については後日まとめます。)


仕事なら既存ライブラリを使ってエラーも少なく、数行のコーディングで済ますのですが、
勉強のため、サードパーティ製のライブラリは使いたくないんです。





ArgumentExceptionはハンドルされませんでした。



指定された値は ':' 区切り記号を含んでいません。 
パラメーター名:header



最初にぶち当たったエラーは、引数エラーです。
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(reqUrl);

string authHeader = "OAuth oauth_consumer_key=\"{0}\" ・・・・・・"

req.Host = "api.twitter.com";
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
req.ContentLength = bodyBytes.Length;
req.Headers.Add("Authorization " + authHeader); 
9行目が原因です。
Authorizationヘッダは”Authorization: OAuth oauth_consumer_key=・・・・・”のように、
「Authorization」のあとにコロン(:)が必要になります。これがないとこのような引数エラーになります。

以下の書き方でもOK
req.Headers.Add("Authorization", authHeader);



ProtocolViolationException はハンドルされませんでした。



コンテンツ本体をこの verb-type では送信できません。



次にあたったのがこのエラー。
タイムラインの取得なので”GET”だろうと思って書いたこの一行
req.Method = "GET";
この”GET”が原因です。
req.Method = "POST";


のように”POST”にするとうまく行きました。


※訂正:2013/06/08

タイムラインの取得ですので、HttpMethodは”GET”が正しいです。
ツイートするの場合は、”POST”でよいと思います。


WebException はハンドルされませんでした。



リモート サーバーがエラーを返しました: (400) 要求が不適切です
{"errors":[{"message":"Bad Authentication data","code":215}]}


次がこのエラー。

ステータスコード400番台はクライアント側のリクエストに問題があります。
Twitter API1.1の仕様上、APIリクエスト制限に達してもこのエラーになるとか…。


原因1:Nonceの生成方法



このエラーは、ライブラリ「OAuthBase.cs」を使って生成した「Nonce」に原因がありました。

アクセストークン取得のとき、「Nonce」は8byteのランダム文字列で良いと書かれていました。
そのため、OAuthBase.csのGenerateNonceメソッドも8byteのランダム文字列を返すようになっていました。

でも実は32byteのランダム文字列にしなければいけません。

Nonce
The oauth_nonce parameter is a unique token your application should generate for each unique request. Twitter will use this value to determine whether a request has been submitted multiple times. The value for this request was generated by base64 encoding 32 bytes of random data, and stripping out all non-word characters, but any approach which produces a relatively random alphanumeric string should be OK here.

引用元:Twitter Developers - Authorizing a request

public virtual string GenerateNonce() {
    // Just a simple implementation of a random number between 123400 and 9999999
    //return random.Next(123400, 9999999).ToString();

    string letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    StringBuilder result = new StringBuilder(32);
    Random random = new Random();
    for (int i = 0; i < 32; ++i)
        result.Append(letters[random.Next(letters.Length)]);
    return result.ToString();
}

元は3行目のように123400~999999からランダムな数字を生成して返していた所を、
下のようにa~z、A~Z、0~9を使って32byteの文字列を返すように修正しました。


※訂正:2013/06/08

OAuthBaseのGenerateNonceメソッドをそのまま使ってもタイムラインの取得ができました。
原因は他にあったようです。


原因2:Authorizationヘッダのダブルクォーテーション



Authorizationヘッダの生成を以下のように行なっていました。

string authHeader = "OAuth "
                 + string.Format("oauth_consumer_key={0}, oauth_nonce={1}, oauth_signature={2}, oauth_signature_method={3}, oauth_timestamp={4}, oauth_token={5}, oauth_version={6}"
                 , CONSUMER_KEY
                 , nonce
                 , signature
                 , "HMAC-SHA1"
                 , timestamp
                 , settings.AccessToken
                 , "1.0"
                 );

 パッと見、何も問題ないように見えますが、実はダブルクオーテーションがありません。
Twitter Developersで調べてみると「oauth_consumer_key="xxxx"」のように設定する必要があるみたいです。

以下のようにダブルクォーテーションをエスケープシーケンスの「\"」に置き換えました。

string authHeader = "OAuth "
                 + string.Format("oauth_consumer_key=\"{0}\", oauth_nonce=\"{1}\", oauth_signature=\"{2}\", oauth_signature_method=\"{3}\", oauth_timestamp=\"{4}\", oauth_token=\"{5}\", oauth_version=\"{6}\""
                 , CONSUMER_KEY
                 , nonce
                 , signature
                 , "HMAC-SHA1"
                 , timestamp
                 , settings.AccessToken
                 , "1.0"
                 );



WebException はハンドルされました



リモート サーバーがエラーを返しました: (401) 許可されていません 
401 Unauthorized : データにアクセスするには認証が必要




こいつのせいで完全にハマってしまいました。 おそらく2.5日は潰しました。

Twitter DevelopersのOAuth Toolを使ってもみました。

OAuth Toolのページ下部の「Request URI」に「https://api.twitter.com/1.1/statuses/user_timeline.json」と入れて
「See OAuth signature for this request」ボタンを押下すると
Signature base string(シグネチャ変換前)や Authorization header(HTTPのヘッダにつける)を生成することができます。
※有効時間は数分です。

生成されたものと自分で生成したものを見比べてみたり、
生成されたものを直接、Authorizationヘッダに設定してみたりしたのですが、
何度やっても401の承認エラーが返ってきます。



試してみた解決策



http://d.hatena.ne.jp/yayugu/20110813/1313220348
yayuguのにっき - TwitterのOAuthでrequest tokenを取得しようとして401 Unauthorizedがでるときの原因と対処法3つ







1.コンピュータの時刻が狂っている
2.アプリがTwitterからSuspendされてる
3.アプリの設定画面のCallbackURLに何も入力されていない

1.コンピュータの時刻も合わせましたが特に変わらず
2.API制限は https://api.twitter.com/1/account/rate_limit_status.json を見ても特に引っかかてません
3.Callbackに「http://127.0.0.0/callback」と入れても特に変わらず
  デスクトップクライアントにも関係するのかな?




https://dev.twitter.com/discussions/12758
Twitter Developers - .NET Fetch user timeline using API 1.1 and oAuth: 401 Unauthorized








1.Signature base string のパラメータの並び順大丈夫?
2.authHeader のパラメータの並び順大丈夫?

どちらもOAuth Toolで作成した文字列と一致していましたがエラーのまま。



その他(参考サイトは忘れました)



1.Signature base stringをHttpUtility.UrlEncodeメソッドを使ってエンコードしてからSignatureを生成する
  ただし、他のURLやAuthHeaderはエンコードしない
2.GetBytesをASCIIではなくUTF-8でエンコードする
3.コンシューマキーやアクセストークンは最新?
4.いっそ一から作り直したら?
1.エスケープシーケンスに変換されましたが、エラーのまま
2.全てのエンコードをUTF-8やASCIIにしても、エラーのまま
3.キーのリセットや、アプリの許可取り消し、ローカルに保存しているキーも初期化しても、エラーのまま
4.何度も作り直しとるわ!!



あとがき



前述の通り、このエントリを書いた翌日にタイムラインを取得することができました。
その内容については次回以降にまとめて投稿いたします。



追記(401:Unauthorizedの原因)




http://stackoverflow.com/questions/3981564/cannot-send-a-content-body-with-this-verb-type
stackoverflow - Cannot send a content-body with this verb-type





このトピックがエラーを解消してくれました!
原因は単純です。


Stream reqStream = req.GetRequestStream();
reqStream.Write(bodyBytes, 0, bodyBytes.Length);

WebResponse res = req.GetResponse();
Stream resStream = res.GetResponseStream();

StreamReader sr = new StreamReader(resStream, enc);
Console.WriteLine(sr.ReadToEnd());

この1~2行目が原因でした。
全然認証と違う部分で、エラー内容と一致しなくてかなり手こずりました。

HttpリクエストをGetRequestStreamで読み込むことはできませんん。
GetResponseで受けてからGetResponseStreamで読み込みます。

下のbodyへの書き込みとGetRequestStreamは不要な処理でした。




追記:2015/11/14 17:30

Twitterで次のような質問をいただいた。

ソースとエラー内容を見せてもらって回答したのが次のツイート

原因は、PINコード取得時とアクセストークン取得時のセッション(nonceやtimestamp、signetureなど)が一致していなかったのが原因。
WindowsFormをつくるときは「Interaction.InputBox」などを使い、PINコード取得からアクセストークン取得までをひとつの処理として行う必要があるので注意。
(もしくはPINコード取得時のセッションを別に所有しておくとか)

とにかく、うまく動作したようでなにより。





以上

0 件のコメント :

コメントを投稿