NuxtでCORSエラーを突破した話
診断サイトを作成し、診断結果に基づいたイメージ画像を生成しPDF出力するという機能を実装しようとしました。その際にCORSエラーにぶつかったのですが、Nuxtの機能を使用して解決できたのでその記録です。
構成
- Nuxt
- OpenAI(診断時のチャット応答)
- DALL-E(画像生成)
- PDF-LIB(PDF出力)
診断時のチャット応答でOpenAIのAPIを使用する関係で、同じAPIで使用できるDALL-Eを使用して画像生成を行いました。
画像生成での問題点
クライアント側で画像生成をしようとすると、CORSエラーに引っ掛かります。
CORSとは?
Webブラウザはセキュリティ上の理由から、通常は「同じオリジン(ドメイン+ポート+プロトコル)」のリソースしか自由に扱えません。
例えば、https://example.com で動いているWebアプリが、直接 https://api.another.com にリクエストを送ってレスポンスを扱おうとすると、ブラウザは「これは別のオリジンだから危険かも」とブロックしてしまいます。
この制約を緩和する仕組みが CORS(Cross-Origin Resource Sharing) です。
APIサーバーがレスポンスヘッダーに
Access-Control-Allow-Origin: https://example.com
のような指定をしていれば、ブラウザは「このオリジンからのアクセスは許可されている」と判断し、リクエストを通します。
DALL-Eなどの画像生成APIを使うとき、クライアント(ブラウザ)から直接APIを呼び出そうとすると問題になるのがCORSです。
-
API提供元が「任意のオリジンからのアクセス」を許可していない
-
Access-Control-Allow-Origin が * でなく制限付き
-
特に画像データを BlobやBase64で扱う処理 をするときに制約に引っかかる
結果として、ブラウザのコンソールに
Access to fetch at 'https://api.xxx.com' from origin 'https://yourapp.com' has been blocked by CORS policy
といったエラーが出てしまいます。
私も、画像をbase64化する処理をしていたのもあって、CORSのエラーが出てしまっていました。
(補足)base64について
Base64(ベース64) は、データを「A〜Z, a〜z, 0〜9, +, /」の64種類の文字で表すエンコード方式です。
もともとは バイナリデータ(画像や音声など)をテキストとして扱えるようにする仕組みです。
この方法には、以下の利点があります。
-
文字だけで表せる
バイナリのままだと送受信できない環境(メールやJSONなど)でも扱える -
どこでも保存しやすい
データベースやテキストファイルに埋め込める -
Webで便利
画像を <img src="data:image/png;base64,..."> のように直接HTMLに埋め込める
サイズが大きくなってしまう、大きな画像には向かないといったデメリットもあるのですが、今回のアプリケーションでは、イメージ画像を1枚生成し、それをブラウザ表示とPDF出力時に使用するというもので、どこかのストレージに保存したりしない想定だったので、利便性のためにbase64化していました。
どうやって解決する?(一般的な解決法)
CORSは「ブラウザから他オリジンにアクセスする場合」に適用されるルールです。
つまり、サーバー側(Node.jsやPythonなどのバックエンド)からAPIにリクエストを送る場合はCORSの制限を受けません。
よって、以下のような実装をすることになります。
-
クライアント → 自分のサーバーに「画像生成して」とリクエスト
-
サーバー → 外部の画像生成APIにアクセスし、結果を取得
-
サーバー → クライアントに画像データ(またはURL)を返す
こうすることで、クライアントは自分のサーバーとだけ通信するのでCORSの制限を回避できます。この方法だとAPIキーを直接フロントに埋め込まずに済むため、セキュリティ面でも良い方法です。
Nuxtの機能を使ってCORSエラーを突破する
NuxtはVue.jsを基本としたJavaScriptフレームワークですが、サーバーサイドのAPIエンドポイントを簡単に作れる仕組みが標準で入っています。
プロジェクトのserver/apiディレクトリにファイルを置くだけで、そのファイルが自動的に API エンドポイントになります。つまり、フロントと同じプロジェクト内にバックエンド API を持てるのです。
今回の画像生成でいうと、以下のような流れになります。
-
フロントエンド(ブラウザ) → 自分の Nuxt アプリの /api/... にリクエスト
-
Nuxt サーバー(server/api 内のコード) → 外部の画像生成 API へリクエスト
-
Nuxt サーバー → レスポンスを受け取り、フロントに返す
つまり、フロントから見れば「同じオリジンの /api/... にリクエストしている」だけ。
CORS 制約は発生せず、外部 API の呼び出しは Nuxt サーバー側で処理してくれます。
Nuxtで標準で入っている機能なので追加で何かを設定する必要もないですし、フロントとバックを同じリポジトリで管理できるため、ソース管理も楽です。
コード(例)
例えば、OpenAIを使用する場合はこんな感じです。
(実際作ったものはPDF生成などでやや複雑なコードになっているのでサンプルを載せておきます)
サーバー側
export default defineEventHandler(async (event) => {
const body = await readBody(event) // フロントからのリクエストデータ
const prompt = body.prompt
// 外部APIをサーバーから呼び出す
const response = await $fetch('https://api.openai.com/v1/images/generations', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: {
prompt,
n: 1,
size: '512x512',
},
})
return response
})
フロント側
<script setup lang="ts">
const prompt = ref("かわいい猫のイラスト")
const generate = async () => {
const res = await $fetch('/api/generate-image', {
method: 'POST',
body: { prompt: prompt.value }
})
console.log(res)
}
</script>
参考
Discussion