ブラウザでdiscord.js使用時のCORSエラーを回避した

ブラウザでdiscord.jsを使ってボットを動かそうとしたらエラーになってしまったので、その原因を調べて、エラーを回避するスクリプトを実装してみました。
※一部の機能しか使えません。

ライブラリ：[discord.js（webpackブランチ）](https://github.com/discordjs/discord.js/tree/webpack)
ブラウザ：Microsoft Edge
Webサーバー：Visual Studio Code + [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)
Node.jsは使用しません。



# 何が問題？

discord.jsのドキュメントのExampleソースコードを実行します。
https://discord.js.org/#/docs/main/stable/topics/web

すると早速ブラウザのコンソールに次のエラーが出てしまいます。

> Access to fetch at 'https://discord.com/api/v7/gateway/bot' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.



# 原因

Discordのドキュメントによると、HTTP APIには有効なUser-Agentを送らなければならないようです。
https://discord.com/developers/docs/reference#user-agent

> Clients using the HTTP API must provide a valid User Agent which specifies information about the client library and version in the following format:
> 
> User Agent Example
> 
> ```
> User-Agent: DiscordBot ($url, $versionNumber)
> ```

よって、それ以外のUser-Agentの場合は未定義です。（User-AgentによってAPIが使えなかったり使えたりしました。）
ブラウザのUser-Agentでエラーになるのはバグではなく仕様ですね。

ぶっちゃけ、ブラウザの開発者ツールでUser-Agentを設定してしまえば直ります。



# エラー回避方法

discord.jsを読み込む前に次のコードを実行します。

```JavaScript
const FETCH = fetch;

window.fetch = async (input, init) => {
  if (input === 'https://discord.com/api/v7/gateway/bot') {
    const data = {
      url: 'wss://gateway.discord.gg',
      session_start_limit: {},
    };
    const headers = {
      'Content-Type': 'application/json',
    };
    return new Response(JSON.stringify(data), { headers });
  }

  return FETCH(input, init);
};
```

とりあえずこれでExampleソースコードが動いてメッセージを受信できるようになりました。

使えるのは一部の機能のみで、残念ながらエラー回避できない機能があります。

メッセージの送信はWebhookを代用します。
https://discord.js.org/#/docs/main/stable/examples/webhook

## 解説

DiscordのAPIは2つあります。

* HTTP API
* Gateway (WebSocket) API

discord.jsはまずHTTP APIにアクセスして、Gateway APIのURLを受け取ってからGatewayに接続しています。
Gateway APIにはUser-Agentの仕様がないのでブラウザからもアクセス可能です。

そこで上記のソースコードで細工して、最初のHTTP APIのアクセスでアクセスせずにGatewayのURLを受け取ったことにしています。
するとGatewayには接続できるので、Gatewayを使う一部機能のみが使えます。

ちなみに、discord.jsではGateway APIのURLを`/gateway/bot`で取得していますが、`/gateway`でも取得できます。
`/gateway`はブラウザからもアクセスできました。認証不要なので例外なのかもしれません。
https://discord.com/developers/docs/topics/gateway#get-gateway
なので`/gateway`から取得するようにしても良いと思います。
ただしエラーも想定した方が良いでしょう。

WebhookはHTTP APIに含まれそうな気がしますが、GitHubなどの連携サービスはおそらく有効なUser-Agent（DiscordBot）を送ってこないので、連携サービスからのアクセスを受け付けるために例外なのかもしれません。



# JavaScriptでUser-Agentを変更できないの？

Edgeでは変更できませんでした。

仕様としてはUser-Agentを変更でき、Firefoxでは変更できるようです。
https://developer.mozilla.org/ja/docs/Glossary/Forbidden_header_name

> 注: User-Agent ヘッダーは仕様としては禁止ではなくなりました (Firefox 43 で実装された forbidden header name list を参照)。 Fetch の Headers オブジェクトや、XHR の setRequestHeader() などでこのヘッダーを設定することが可能です。

ということで、消してしまったFirefoxをインストールして検証してみます。

```JavaScript
const FETCH = fetch;

window.fetch = (input, init) => {
  init.headers['User-Agent'] = 'DiscordBot';
  return FETCH(input, init);
};
```

コンソールに次のエラーが出ました。

> クロスオリジン要求をブロックしました: 同一生成元ポリシーにより、https://discord.com/api/v7/gateway/bot にあるリモートリソースの読み込みは拒否されます (理由: CORS プリフライト応答からのヘッダー ‘Access-Control-Allow-Headers’ によりヘッダー ‘user-agent’ が許可されていない)。

Discord APIからのレスポンスヘッダーには下記のAccess-Control-Allow-Headersがありました。

> access-control-allow-headers: Content-Type, Authorization, X-Track, X-Super-Properties, X-Context-Properties, X-Failed-Requests, X-Fingerprint, X-RPC-Proxy, X-Debug-Options, x-client-trace-id, If-None-Match, X-RateLimit-Precision

確かに、User-Agentは許可されているヘッダーに含まれていませんでした。
ダメみたいですね。



# FAQ

[discord.js](https://github.com/discordjs/discord.js)と[Discord Official API Documentation](https://github.com/discord/discord-api-docs)のIssuesで見つけたブラウザ関連の質問と答えを超簡単にまとめました。

## Q. [discord.js]Discord APIへのアクセスで403レスポンスが来る

`https://discord.com/api/v7/gateway/bot`へのアクセスで常に403レスポンスが来る。
2020/11/7に廃止される`discordapp.com`に変更するしか接続方法がない。

A. discord.jsではどうにもできません。（超要約）

https://github.com/discordjs/discord.js/issues/4710

## Q. [Discord]APIがCORSに対応していない

A. CORSに対応しています。何らかのエラーがあるとCORSエラーになります。

https://github.com/discord/discord-api-docs/issues/1873

## Q. [Discord]ブラウザのUser-Agentを送るとエラーになる

A. User-Agentのドキュメントを参照してください。 https://discord.com/developers/docs/reference#user-agent

https://github.com/discord/discord-api-docs/issues/2050



# まとめ

私（CORS）は許そう
だがこいつ（User-Agent）が許すかな！