Next.js の Experimental test mode for Playwright でのナビゲーション時に Server Components での API リクエストエラー調査
Next.js の Experimental test mode for Playwrightを試していた際に、ナビゲーション時に Server Components での API リクエストでエラーが発生した。
それが発生する原因を調査したのでメモしておく。
以下が本事象を再現する Example は以下の通り。
アプリケーション:
import Link from "next/link";
export default function Page() {
return <Link href="/hello">リンク</Link>;
}
import { Suspense } from "react";
export default async function Page() {
const res = await fetch("http://localhost:3000/api/foo");
if (!res.ok) {
throw new Error("Failed to fetch");
}
const data = await res.json();
return (
<div>
{data.text}
<Suspense fallback="loading...">
<Example />
</Suspense>
</div>
);
}
async function Example() {
const res = await fetch("http://localhost:3000/api/bar");
if (!res.ok) {
throw new Error("Failed to fetch");
}
const data = await res.json();
return <div>{data.text}</div>;
}
テスト:
import { test } from "next/experimental/testmode/playwright";
import { expect } from "@playwright/test";
import { setTimeout } from "timers/promises";
test("/hello へのページ遷移テスト", async ({ page, next }) => {
await page.goto("/");
next.onFetch(async (request) => {
if (request.url === "http://localhost:3000/api/foo") {
return new Response(
JSON.stringify({
text: "foo",
}),
{
headers: {
"Content-Type": "application/json",
},
},
);
}
if (request.url === "http://localhost:3000/api/bar") {
await setTimeout(1000);
return new Response(
JSON.stringify({
text: "bar",
}),
{
headers: {
"Content-Type": "application/json",
},
},
);
}
return "abort";
});
const link = await page.getByRole("link", { name: "リンク" });
expect(link).toBeVisible();
await link.click();
});
この状態で VSCode でテストを実行すると、テスト自体はパスするが Next.js サーバー側でエラーが発生してしまう。Playwright のテスト中 Next.js サーバー側の stdout を出力するように設定する(webServer.stdout
を pipe
にする)ことで、以下のような Server Components での API リクエストエラーを確認できる。
⨯ TypeError: fetch failed
at async Page (./src/app/hello/page.tsx:12:17)
digest: "255610910"
Cause: AggregateError
at internalConnectMultiple (node:net:1114:18)
at afterConnectMultiple (node:net:1667:5)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
code: 'ECONNREFUSED',
[errors]: [
Error: connect ECONNREFUSED ::1:55924
at createConnectionError (node:net:1634:14)
at afterConnectMultiple (node:net:1664:40)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -61,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '::1',
port: 55924
},
Error: connect ECONNREFUSED 127.0.0.1:55924
at createConnectionError (node:net:1634:14)
at afterConnectMultiple (node:net:1664:40)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -61,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 55924
}
]
}
内容としては 127.0.0.1:55924
に接続しようとしたけどできなかったというエラー。
※ ポート番号はその人の環境によって異なる
「アプリケーションから 127.0.0.1:55924
にリクエストを送った覚えはないけど、これは一体何のリクエスト?」と疑問に思う方もいるかもしれません。これに関して、Next.js Test Mode の仕組みを簡単に説明する。
Next.js Test Mode を有効にすると、Next.js で使用しているグローバル fetch
が上書きされ、Playwright Worker で起動したプロキシサーバー(127.0.0.1:55924
)にリクエストが送られるようになる。このプロキシサーバーがモックレスポンスを返す仕組み。ここら辺の実装一通り追ったが中々面白い。
詳しくは Quramy さんの記事で紹介されている。
したがって、このエラーは、モックレスポンスを返すプロキシサーバーが起動していない状態で fetch
リクエストが実行されたために発生してしまう。テストが終了するとプロキシサーバーも停止するため、テスト終了後にリクエストが送られていることが原因。
このエラーを回避するには API のレスポンスを待ってからテストを終了するように修正するだけ。
ただし、foo のレスポンスを待機しても、bar のレスポンスでエラーが発生するため注意。
test("/hello へのページ遷移テスト", async ({ page, next }) => {
// ...
await link.click();
+ await expect(page.getByText("foo")).toBeVisible();
});
ちゃんと、ページ上での全ての fetch のレスポンスを待機する必要がある。また、SSR Streaming を使用しているケースにおいては Streaming 完了を待つ前にテストが終了してしまうと UND_ERR_SOCKET エラーが発生してしまう。そのため Streaming についても必ず待つようなテストコードにしよう。
test("/hello へのページ遷移テスト", async ({ page, next }) => {
// ...
await link.click();
+ await expect(page.getByText("foo")).toBeVisible();
+ await expect(page.getByText("bar")).toBeVisible();
});