Next.js でも 418 I'm a teapot したい
418 I'm a teapot って?
HTTP の
418 I'm a teapot
クライアントエラーレスポンスコードは、サーバーが、自身がティーポットであることを理由としてコーヒーを入れることを拒否することを示します。[1]
- 2.3.2 418 I'm a teapot | RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)
- 15.5.19. 418 (Unused) | RFC 9110: HTTP Semantics
Google が 418 I'm a teapot
を返却するエンドポイントを持っていることで有名です.
Error 418 (I’m a teapot)!? | google.com
ということで, Next.js で 418 I'm a teapot
を返却するエンドポイントを作ってみます.
環境
Next.js App Router を使用します.
- Next.js 14.1.0
- React 18
ErrorPage を使った実装
Next.js には ErrorPage
というコンポーネントが用意されているため, これを用いることで Next.js のデフォルトのエラーページをカスタマイズすることができます.
import DefaultErrorPage from "next/error";
export default function Home() {
return <DefaultErrorPage statusCode={418} title="I'm a teapot" />;
}
実際にサーバーを立ててブラウザで確認すると, "418 I'm a teapot" が表示されています.
しかし, HTTPリクエストを見てみると, レスポンスのステータスコードは 200
になっています.
$ curl -I localhost:3000
HTTP/1.1 200 OK
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url, Accept-Encoding
Cache-Control: no-store, must-revalidate
X-Powered-By: Next.js
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Keep-Alive: timeout=5
ここで改めて Google の 418 I'm a teapot
を返却するエンドポイント[2]を見てみると, レスポンスのステータスコードは 418
になっています[3].
$ curl -i -L google.com/teapot
HTTP/1.1 301 Moved Permanently
Location: https://www.google.com/teapot
~~~
HTTP/2 418
content-type: text/html; charset=ISO-8859-1
content-security-policy: object-src 'none';base-uri 'self';script-src 'nonce-Ie5OxLrEPqkNSmMmBTstIA' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/xsrp
server: gws
cache-control: private
x-xss-protection: 0
x-frame-options: SAMEORIGIN
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
accept-ranges: none
vary: Accept-Encoding
そこで Next.js でもHTTPステータスコードとして 418
を返却できるようにします.
route.js によるHTTPレスポンスのカスタマイズ
App Router では page.js
を持つエンドポイントは全て 200
または 404
を返します. 現状では pages.js
を使ってHTTPステータスコードを変更することはできません[5]. そこで route.js
を使ってHTTPレスポンスをカスタマイズします.
route.js
では NextResponse
を使ってHTTPレスポンスをカスタマイズすることができます.
export async function GET(request: Request) {
return new NextResponse(body, {
status: 418,
statusText: "I'm a teapot.",
headers: {
"Content-Type": "text/html",
},
});
}
次にレスポンスボディを作成します. JSXを使ってHTMLを生成したいところですが, Next.js では react-dom/server
を使ってHTMLテキストを生成するとビルドエラーになります. そのため, react-dom/server
を使わずに生のHTMLテキストを使ってそのままレスポンスとして返しましょう. HTMLテキストは先ほど ErrorPage
を使ってレンダリングされたものをそのままコピペして使います.
const body = `
<html>
<head>
<meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>I'm a teapot</title>
<meta name="description" content="I'm a teapot"/>
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/>
<script src="/_next/static/chunks/polyfills.js" noModule=""></script>
</head>
<body style="margin: 0;line-height: inherit;">
<div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center">
<div style="line-height:48px">
<style>
body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}
</style>
<h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">${error.code}</h1>
<div style="display:inline-block">
<h2 style="font-size:14px;font-weight:400;line-height:28px">${error.message}</h2>
</div>
</div>
</div>
</body>
</html>
`;
これでHTTPステータスコードとして 418
を返しつつ, HTMLテキストも返すことができます.
react-dom/server を使ったHTMLテキストの生成
とはいえHTMLテキストを生で扱うのはさすがにつらすぎる, というか React の意味がないので, JSXからHTMLテキストを生成できるようにします. リクエストハンドラの中で react-dom/server
を動的にインポートすることで, ごまかすことができます[6].
export async function GET(request: Request) {
const ReactDOMServer = (await import("react-dom/server")).default;
const component = <BodyElement />;
const bodyHtml = Buffer.from(ReactDOMServer.renderToString(component));
return new NextResponse(bodyHtml, {
status: error.code,
statusText: error.message,
headers: {
"Content-Type": "text/html",
},
});
}
HTMLテキストからJSXへの変換は GitHub Copilot なんかを使って翻訳すると楽です.
これでHTTPステータスコードとして 418
を返却しつつ, レスポンスボディでHTMLを返却することができました.
$ curl -I localhost:3000/teapot
HTTP/1.1 418 I'm a teapot.
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url
content-type: text/html
Connection: keep-alive
Keep-Alive: timeout=5
Next.js App Router はまだまだ知見が少なく, 少し凝ったことをしようとすると苦労することが多いです. Pages Router でできたことが App Router ではできないことも多いため, 慎重に使うことが求められそうです.
-
cURL コマンドを使う場合,
-I
オプションを使うと200
が返ります. ↩︎ -
以下でHTTPステータスコードを変更できないか議論されていますが, 2024年4月現在では変更できないようです. Support custom `HTTP Status Code` for Server Components · vercel/next.js · Discussion #53225 ↩︎
-
以下を参考. https://github.com/vercel/next.js/issues/43810#issuecomment-1462075524 ↩︎
Discussion
Middlewareを使用したほうがスマートに実装できそうな気がしました。
これでページコンポーネント
/app/teapot/page.tsx
の方も自由に実装できそうです。