本記事は ZOZO Advent Calendar 2025 シリーズ 2 の 20 日目の記事です。
概要
\ Uh oh! Something went wrong. /
Looker Extensionの開発中、このエラーメッセージに悩まされた経験はありませんか?
Cloud Runのログは200 OKなのに、Looker側では500 Internal Server Error。原因を探っても手がかりがない...。
本記事では、私がGemini in Looker(dashboard-summarization)を実装した際に遭遇したUnicodeエンコーディング問題とその解決策について紹介します。結論として、LookerプロキシのUnicode処理問題をBase64エンコーディングで回避できました。
前提
本記事は以下の環境・知識を前提としています。
- Looker Extensionの開発経験
- TypeScript/JavaScriptの基本的な知識
- Unicode/文字エンコーディングの基本理解
Looker Extension Framework自体の詳細については公式ドキュメントを参照してください。
原因
どうやら原因は以下のようでした。
Looker Extension (Frontend)
↓
Looker内部プロキシ ← ここで問題発生
↓
外部API (Cloud Run)
Looker内部プロキシがUnicode文字を含むレスポンスを正しく処理できず、500エラーとなっていました。
解決策
Unicode文字を安全に送信するため、Base64エンコーディングで対応しました。
- バックエンドでJSONレスポンスをBase64エンコードして送信
- フロントエンドでBase64デコードしてからJSONパース
バックエンド側の実装
// Before: 直接JSONを返す(問題のあるコード)
app.post('/generateSummary', (req, res) => {
const result = {
summary: "商品の売上が好調です", // 日本語が問題を引き起こす
insights: ["..."]
};
res.json(result);
});
// After: Base64エンコードして返す(解決策)
app.post('/generateSummary', (req, res) => {
const result = {
summary: "商品の売上が好調です",
insights: ["..."]
};
// JSONをBase64エンコード
const base64Response = Buffer.from(JSON.stringify(result)).toString('base64');
res.send(base64Response);
});
フロントエンド側の実装
// base64Helper.ts
export const decodeBase64Response = (base64String: string): any => {
try {
// UTF-8対応のデコード処理
const bytes = Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
const decoder = new TextDecoder('utf-8');
const jsonString = decoder.decode(bytes);
return JSON.parse(jsonString);
} catch (error) {
console.error('Base64デコードエラー:', error);
throw new Error('レスポンスのデコードに失敗しました');
}
};
// API呼び出し側の修正
const fetchSummary = async (data: any) => {
try {
const response = await extensionSDK.serverProxy(`${apiUrl}/generateSummary`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
// Base64レスポンスをデコード
const decodedResult = decodeBase64Response(response.body);
return decodedResult;
} catch (error) {
console.error('API呼び出しエラー:', error);
throw error;
}
};
実装上の重要なポイント
1. TextDecoderの使用
単純なatob()だけでなく、TextDecoderを使用することで、マルチバイト文字を正しく処理できます。
// ❌ 単純なatobだけでは日本語が化ける可能性
const jsonString = atob(base64String);
// ✅ TextDecoderを使用してUTF-8として正しくデコード
const bytes = Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
const decoder = new TextDecoder('utf-8');
const jsonString = decoder.decode(bytes);
2. エラーハンドリング
Base64デコード処理では適切なエラーハンドリングが重要です。
export const decodeBase64Response = (base64String: string): any => {
try {
// デコード処理
} catch (error) {
console.error('デコードエラー詳細:', {
input: base64String.substring(0, 100) + '...', // 最初の100文字のみログ
error: error.message
});
throw new Error('レスポンスの処理中にエラーが発生しました');
}
};
3. 全てのAPIエンドポイントで統一
この問題は日本語を含むレスポンスで発生するため、全てのAPIエンドポイントで統一してBase64エンコーディングを適用することをお勧めします。
Base64エンコーディングのオーバーヘッドは?
「Base64にするとデータ量増えない?」と思われるかもしれません。
- データサイズは約33%増加(Base64の特性)
- CPU使用量はエンコード・デコード処理による軽微な増加
// サイズ比較例(Node.js環境)
const originalJson = JSON.stringify({summary: "商品の売上分析結果"});
console.log('元のサイズ:', Buffer.byteLength(originalJson, 'utf-8')); // 例: 42バイト
const base64String = Buffer.from(originalJson).toString('base64');
console.log('Base64サイズ:', base64String.length); // 例: 56文字(約33%増加)
ただしdashboard-summarizationのようなテキストデータでは実用上無視できるレベルかと思います。
まとめ
今回はdashboard-summarizationで発生しましたが、以下の条件に当てはまる場合は同様の問題が発生する可能性があります。
- 日本語などのマルチバイト文字を含むAPIレスポンス
- Looker Extensionでの外部API連携
- プロキシ経由での通信
いろいろ疑って調べた末にたどり着いた回避策なので、同様の問題に遭遇した方の参考になれば幸いです。