GitHub APIでリモートリポジトリを作成できた。エラーもなし。だが、リクエスト後の処理を思うように実装できない疑惑。
ブツ
インストール&実行
NAME='Electron.Node.js.https.request.20220822091123'
git clone https://github.com/ytyaru/$NAME
cd $NAME
npm install
npm start
db/setting.json
のrepository
に任意のユーザ名、リポジトリ名(mytestrepo
等)とrepo
スコープ権限を持ったトークンをセットしファイル保存するdst/mytestrepo/.git
が存在しないことを確認する(あればdst
ごと削除する)- Ctrl+Shift+Rキーを押す(リロードする)
git init
コマンドが実行されるrepo/リポジトリ名
ディレクトリが作成され、その配下に.git
ディレクトリが作成される
- createRepo実行後、端末に以下のような成功メッセージが表示される
...
statusCode: 201
headers: {
...
}
{"id":...
statusCode: 201
はcreateRepoのリクエストが成功したときの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ハラスメント、テクハラってことね。
ググったらマジであるらしいテクハラ。「こんなこともできないのか」「よく今までやってこれましたね」と侮辱されるらしい。いやそれむしろこっちのセリフなんですけど。
心構え
- ライブラリは手動でデバッグして仕様を把握する
- ドキュメントは矛盾を指摘して突きつける
- そうしてはじめて動くコードが書ける
って話ね。途中で挫折するやつが悪いと。いじめられる奴にも問題があると。ふーん。
オーケーわかった、お前たちは敵だ。上等だよコノヤロー。やってやんよコンチクショウ。絶対思い通りの処理を書いてやる。お前ら間違いをこっぱみじんにしてやりたいことやってやんよ。怒りと憎しみをパゥワーに変えて生産してやる。私流アンガーマネジメントみせてやる。
勝利条件はいたってシンプル。ただ実践するのが大変なだけ。
- 人に期待しない
- 自分でやる
- できるまでやる
なんでこんなに大変なんだろう。楽するためにライブラリ使ってるはずなのに。
なにかがおかしい。なにもかもがおかしい。ならば狂ってでもやってやる。