GitHub APIでリモートリポジトリを作成できた。エラーもなし。だが、リクエスト後の処理を思うように実装できない疑惑。

ブツ

インストール&実行

NAME='Electron.Node.js.https.request.20220822091123'
git clone https://github.com/ytyaru/$NAME
cd $NAME
npm install
npm start
  1. db/setting.jsonrepositoryに任意のユーザ名、リポジトリ名(mytestrepo等)とrepoスコープ権限を持ったトークンをセットしファイル保存する
  2. dst/mytestrepo/.gitが存在しないことを確認する(あればdstごと削除する)
  3. Ctrl+Shift+Rキーを押す(リロードする)
  4. git initコマンドが実行される
    • repo/リポジトリ名ディレクトリが作成され、その配下に.gitディレクトリが作成される
  5. createRepo実行後、端末に以下のような成功メッセージが表示される
...
statusCode: 201
headers: {
  ...
}
{"id":...

statusCode: 201createRepoのリクエストが成功したときのHTTPコードである。最後の{"id":...はそのリポジトリの情報。

リクエストに成功しました。めでたしめでたし。というわけでもない。じつはリクエスト後の処理が思うように実装できない疑いがある。

経緯

色々ありすぎて混乱してきたので最初から経緯をまとめておく。

ElectronのAPI net.request を使ってGitHub APIを使いリモートリポジトリを作成したい。そこでまずは引数が不要なため比較的簡単に発行できそうなGitHub APIのusersを叩いてみた。An object could not be cloned.エラーにより阻まれるも、IPC通信インタフェース引数に関数を含めないようにすることで解決できた。次にusersからcreateRepo APIに変えてリポジトリ作成を試みたが、謎のダイアログエラーnet:ERR_CONNECTION_REFUSEDが出た。このエラーは原因についての説明がないので打つ手なし状態に陥った。

そこで今回は、そもそもElectron API net.request の使用をやめて、Node.js APIでリクエストできないか試してみた。元々ElectronはNode.jsのパッケージなので、大本はNode.jsのはず。そのNode.jsにHTTPリクエストするAPIがないわけがない。というわけでググってみた所、https.requestを見つけた。今回はこのhttps.requestでGitHub APIのcreateRepoをリクエストしてみた。

記事 GitHub API
Electronのnet.requestでWebAPIを叩くも失敗(エラー) users
Electronのnet.requestでWebAPIを叩く(成功) users
Electronのnet.requestで謎エラー(net:ERR_CONNECTION_REFUSED) createRepo

コード抜粋

main.js

const { app, BrowserWindow, ipcMain, dialog, net } = require('electron')
const util = require('util')
const childProcess = require('child_process');

ipcMain.handle('exists', (event, path)=>{ return fs.existsSync(path) })
ipcMain.handle('mkdir', (event, path)=>{
    if (!fs.existsSync(path)) {
        fs.mkdirSync(path, {recursive:true})
    }
})
ipcMain.handle('shell', async(event, command) => {
    const exec = util.promisify(childProcess.exec);
    return await exec(command);
})

mkdirSyncは今回見つけたAPIのひとつ。これまではシェルコマンドのmkdirで実行していたが、それが存在しないOSだと動作しない。そこでNode.jsのAPIであるmkdirSyncでディレクトリを作成するようにした。

existsSyncは指定したファイル・ディレクトリパスが存在すればtrue、なければfalseを返すAPI。

これらはローカルリポジトリ作成するときに使う。

以下がHTTPリクエストに成功したコード。

ipcMain.handle('createRepo', async(event, username, token, repo, description)=>{
    const url = 'https://api.github.com/user/repos'
    const options = {
        'method': 'POST',
        'headers': {
            'Authorization': `token ${token}`,
            'Content-Type': 'application/json',
            'User-Agent': username,
        },
        'body': {
            'name': repo,
            'description': description,
        },
    }
    const request = https.request(url, options, (res)=>{
        console.log('statusCode:', res.statusCode);
        console.log('headers:', res.headers);
        res.setEncoding('utf8');
        res.on('data', (d) => {
            console.log(d)
        });
    }).on('error', (e) => {
        console.error(e);
    });
    if (options.hasOwnProperty('body')) {
        request.write(JSON.stringify(options.body));
    }
    request.end();
});

ちなみに、以下のように引数にonData,onErrorのようなコールバック関数を渡すとAn object could not be cloned.エラーになってしまうため使えなかった。

ipcMain.handle('httpsRequest', async(event, url, options, onData=null, onError=null)=>{
    const request = https.request(url, options, (res)=>{
        console.log('statusCode:', res.statusCode);
        console.log('headers:', res.headers);
        res.setEncoding('utf8');
        res.on('data', (d) => {
            console.log(d)
            //process.stdout.write(d);
            if (onData) { onData(JSON.parse(d), res) }
        });
    }).on('error', (e) => {
        console.error(e);
        if (onError) { onError(e) }
    });
    if (options.hasOwnProperty('body')) {
        request.write(JSON.stringify(params.body));
    }
    request.end();
})
ipcMain.handle('createRepo2', async(event, username, token, repo, description, onData=null, onError=null)=>{
    const url = 'https://api.github.com/user/repos'
    const options = {
        'method': 'POST',
        'headers': {
            'Authorization': `token ${token}`,
            'Content-Type': 'application/json',
            'User-Agent': username,
        },
        'body': {
            'name': repo,
            'description': description,
        },
    }
    const request = https.request(url, options, (res)=>{
        console.log('statusCode:', res.statusCode);
        console.log('headers:', res.headers);
        res.setEncoding('utf8');
        res.on('data', (d) => {
            console.log(d)
            if (onData) { onData(JSON.parse(d), res) }
            //return JSON.parse(d)
        });
    }).on('error', (e) => {
        console.error(e);
        if (onError) { onError(e) }
        //return e
    });
    if (options.hasOwnProperty('body')) {
        request.write(JSON.stringify(options.body));
    }
    request.end();
})

preload.js

const {remote,contextBridge,ipcRenderer} =  require('electron');
contextBridge.exposeInMainWorld('myApi', {
    exists:async(path)=>await ipcRenderer.invoke('exists', path),
    mkdir:async(path)=>await ipcRenderer.invoke('mkdir', path),
    shell:async(command)=>await ipcRenderer.invoke('shell', command),

    createRepo2:async(username, token, repo, description, onData=null, onError=null)=>await ipcRenderer.invoke('createRepo2', username, token, repo, description, onData, onError),
})

ちなみに、以下のように引数にonData,onErrorのようなコールバック関数を渡すとAn object could not be cloned.エラーになってしまうため使えなかった。

    createRepo:async(token, name, description, onData=null, onError=null)=>await ipcRenderer.invoke('createRepo', token, name, description, onData, onError),
    httpsRequest:async(url, options, onData=null, onError=null)=>await ipcRenderer.invoke('httpsRequest', url, options, onData, 
    createRepo:async(username, token, repo, description)=>await ipcRenderer.invoke('createRepo', username, token, repo, description),

renderer.js

await window.myApi.createRepo(
    setting.github.username, 
    setting.github.token, 
    setting.github.repo, 
    'リポジトリの説明',
)

以下はコールバック関数を渡すせいでAn object could not be cloned.エラーになってしまう。

await window.myApi.createRepo2( // Uncaught (in promise) Error: An object could not be cloned.
    setting.github.username, 
    setting.github.token, 
    setting.github.repo, 
    'リポジトリの説明',
    (json, res)=>{
        console.debug(res)
        console.debug(json)
        console.debug('GitHub リモートリポジトリ作成完了!')
    },
    (e)=>{
        console.error(e)
        console.debug('GitHub リモートリポジトリ作成失敗……')
    }
)

どうやら、とにかくコールバック関数を渡すことはできないらしいとわかった。それはnet.requestだろうがhttps.requestだろうが関係ない。なら一体、どうすればリクエスト後の処理ができるの? ……え、まさか、できないの? そんなバカなこと、あるわけないよね?

User agent の必要性

GitHub APIでcreateRepoを叩いたら、以下403エラーになった。

statusCode: 403
headers: {
  'cache-control': 'no-cache',
  'content-type': 'text/html; charset=utf-8',
  'strict-transport-security': 'max-age=31536000',
  'x-content-type-options': 'nosniff',
  'x-frame-options': 'deny',
  'x-xss-protection': '0',
  'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'",
  connection: 'close'
}

Request forbidden by administrative rules. Please make sure your request has a User-Agent header (https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required). Check https://developer.github.com for other possible causes.

ググったらGitHubのページUser agent の必要性に書いてあった。

すべての API リクエストには、有効な User-Agent ヘッダを含める必要があります。 User-Agent ヘッダのないリクエストは拒否されます。 User-Agent ヘッダの値には、GitHub のユーザ名またはアプリケーション名を使用してください。 そうすることで、問題がある場合にご連絡することができます。

つまりGitHub APIを叩くときは必ずUser-Agentヘッダが必要で、その値にはユーザ名かアプリ名を使えと。

main.jsのコード抜粋は以下。'User-Agent': username,のところがポイント。これを追加したら403エラーが解決した。

ipcMain.handle('createRepo', async(event, username, token, repo, description)=>{
    const url = 'https://api.github.com/user/repos'
    const options = {
        'method': 'POST',
        'headers': {
            'Authorization': `token ${token}`,
            'Content-Type': 'application/json',
            'User-Agent': username,
        },
        'body': {
            'name': repo,
            'description': description,
        },
    }
...

やはりMSの公式ドキュメントはまちがっている

あれれー? おっかしいぞぉ? Electronのnet.requestでWebAPIを叩く(成功)のときはUser-Agentヘッダつけてなくても成功したよ? このときはusers APIだったけども。users APIのときはUser-Agentヘッダ不要ってこと? でもドキュメントには「すべての API リクエストには、有効な User-Agent ヘッダを含める必要があります。」って明記してるよ? 矛盾してない?

さては、まーたドキュメントのやつ嘘ついてたのかよ。一体いくつ間違いを仕込んでやがるんだ。勘弁してほしい。Electron, Node.js, GitHub, あらゆるものたちに振り回されている私……。もうやめて。つらい。

ITハラスメント

もういい。誰も信じない。何も信じない。人は嘘をつく。ライブラリはいらぬ苦労を作り出すお役所仕事。ドキュメントは間違いを混入させた怪文書。それが常識ってことね。嫌がらせなのね。ITハラスメント、テクハラってことね。

ググったらマジであるらしいテクハラ。「こんなこともできないのか」「よく今までやってこれましたね」と侮辱されるらしい。いやそれむしろこっちのセリフなんですけど。

心構え

  1. ライブラリは手動でデバッグして仕様を把握する
  2. ドキュメントは矛盾を指摘して突きつける
  3. そうしてはじめて動くコードが書ける

って話ね。途中で挫折するやつが悪いと。いじめられる奴にも問題があると。ふーん。

オーケーわかった、お前たちは敵だ。上等だよコノヤロー。やってやんよコンチクショウ。絶対思い通りの処理を書いてやる。お前ら間違いをこっぱみじんにしてやりたいことやってやんよ。怒りと憎しみをパゥワーに変えて生産してやる。私流アンガーマネジメントみせてやる。

勝利条件はいたってシンプル。ただ実践するのが大変なだけ。

  • 人に期待しない
  • 自分でやる
  • できるまでやる

なんでこんなに大変なんだろう。楽するためにライブラリ使ってるはずなのに。

なにかがおかしい。なにもかもがおかしい。ならば狂ってでもやってやる。