概要
Angular (+ express) + Node.js でバックエンドに API 通信をさせた時にエラーでハマった経緯と、その解消策の備忘録
先に結論から言うと
「Nginx の設定をミスったら、バックエンド通信に失敗するよ」と言う話。
経緯
Angular + express で MEAN スタックで作成した Web アプリ。
今までは普通に、開発 ➡️ サーバへデプロイ ➡️ 実機で動作確認、で問題なく動いていた。
ただし、ドメイン取得して、サーバのトップレベルで動作する Web サイトとして作っていた。
今回は、サブディレクトリでのみ動作する Web サイトとして開発。
つまり...
-
https://example.com
▶︎ 別の Web サイト(静的 HTML ページ) -
https://example.com/my-app
▶︎ 今回の Angular サイト
としようとした。
実際に、Angular でアプリを開発して配置。
事象と解消手順
事象1: 画面表示でエラー発生
https://example.com/my-app
にアクセスしたら、テキストだけ出るけど装飾系が描画されない。と言う中途半端な状態で画面表示。表示が壊れている状態。。。
もちろん、https://example.com
は問題なく規定の Web ページが表示される。
▶︎ 解消: 「base href」を設定したら解消
index.html の <base href="/">
が <base href="/my-app/">
となると解消する。
しかし、ローカル環境では必要ない...。
なので、以下のとおり、package.json のビルドコマンドに --base-href
オプションを指定することで、ビルド時に「base href」を変え分けられるようにした。
:
"scripts": {
// 起動コマンド
"dev": "node ./dist/server/server.js",
// ビルドコマンド (ng build に --base-href オプション追加)
"build": "npm run build:client && npm run build:server",
"build:client": "ng build --prod --base-href=/my-app/ && ng run my-app:server",
"build:server": "tsc --project server --outDir dist --allowjs true",
:
},
:
事象2: バックエンドへの通信時にエラー発生
「事象1」が解消し、画面表示はできるようになったが、
今度は、API でバックエンドにリスト取得する処理を実装したら以下のエラー事象が発生。
ローカルだと取得できてたのに、サーバだとリスト取得できない。。。(本来なら JSON のリストデータが返却される予定)
ブラウザコンソールを開いてみたら、以下のエラーログを吐いていた。
# 発生したエラー
error: SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse...
headers: t {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}
message: "Http failure during parsing for https://iexample.com/portal/api/my-app/api/heros"
name: "HttpErrorResponse"
ok: false
status: 200
statusText: "OK"
url: "https://example.com/my-app/api/heros"
バックエンド通信で、HTTP 結果コードは 200 が返っているが、返却された後に何かエラーが出ている。
error
の部分を開いて詳しく見てみると
SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse (<anonymous>) at XMLHttpRequest
さらに、すぐ下に
text: "<!DOCTYPE html><html lang="ja"><head>↵ <meta charset="utf-8">↵ <title>My App</title>↵
JSON データ、もしくは、 JSON のリストデータが返却されるべき箇所で、HTML を返しているために、JSON.parse() でエラーが発生したことが原因らしい。
長く悩んだが、よく考えたらそりゃそうだ。Web サービスで画面用のルーティングしか設定してなかった。
▶︎ 解消: Nginx のルーティングに API 専用のルートを追加したら解消
server {
# http を https にリダイレクト
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# *** 追加: 以下の API 用ルーティングを追加 ↓↓↓ ******
location /my-app/api/ {
proxy_pass http://127.0.0.1:3000/api/;
proxy_redirect default;
}
# *** 追加 ↑↑↑ *********************************
location /my-app/ {
proxy_pass http://127.0.0.1:3000/;
proxy_redirect default;
}
location / {
root /var/www;
index index.php index.html index.htm;
}
# 404, 50x 等 エラー系のルートが以下にある
}
設定後、nginx -s reload
で Nginx を再起動。
Angualar も再デプロイして起動し直し、ブラウザから再度アクセスしてみたら、
エラーは出なくなり、問題なく目的の JSON リストデータが返却されて、画面に表示された。
ハマりどころ
そもそも「Angular の設定方法やロジックに原因がある」として、ずーーーーっと、アプリ設定のネット記事を漁っていたが、そこが間違いだった。
原因の切り分けフロー
HTTP 結果は 200 で、エラーの吐き方が、JSON パースエラー: クラサバ通信はできている
→ リクエストとレスポンスの機構を改めて確認: SSR で実装しているので、リクエスト/レスポンスは HTML を返却するのと JSON データを返却する2種類がある
→ ローカル環境で、ブラウザに GET メソッドの API を叩いてみる: JSON データが返り、画面に表示されることを確認
→ 公開サーバで、同じくブラウザに GET メソッドの API を叩いてみる: トップページが再表示された = HTML が返却された
→ Web サーバのリクエスト URI に対するルーティング先を修正すべきことに気づく
→ Nginx の config を修正して動作確認
→ 問題解消 !!!
こんな感じだった。
エラーの切り分けを丁寧に行い、対応していくことは大事だ。
以上、Web サービスの設定は、大した内容ではないが間違えると結構ハマる。