Чаму WebSockets застаюцца ў маршрутах API Next.js і як спецыяльны сервер гэта выправляе
Дазвольце даклэ расказаць, чаму сервер WS у маршруце pages/api ніколі не завершае процэс аб’явлення з’ўязку, і як самостойна кераваць запускам апгрэйда за дапамою спецыяльнага сервера Next.js.
Крыніцы: The Next.js WebSocket Trap: Why Your pages/api Handler Never Upgrades
Звычным першым спробам выкарыстоўваць WebSockets у Next.js ёсць стварэнне сервера ws у файле pages/api/socket.ts і падключэнне да яго з браузера. Рэзультат ёсць заплутаны: ніякіх памылак, ніяких запаведзей, проста сокет застаецца ў стане CONNECTING пакуль браузер не выканае таймаут. У гэтым кяліку пояснюецца, чаму так адбываецца, паказваецца, як перэявіць кераванне HTTP-серверам, каб процес падключэння дыйсна завершыўся, а таксама рассматрываюцься меры масштабавання і ліміты хостынгу, якія вы прыймаеце ў замене.
Чаму API-шлях ніколі не можа падняць ранг з’ѐеднання
Маршрут API у Next.js — это звычны каналобрабатар HTTP-запросаў. Ён запускаецца пасля таго, як запрос вялікі як звычны HTTP-запрос. Аднак рукаводжэнне WebSocket — это HTTP-запрос, які несе загалоўку Upgrade, і яго трэба пераследзіць у змаганні upgrade сервера Node, прычым роутер не можа яго адобрабатаваць. Калі функцыя маршрута востанні раз выконваецца, такая можлівасць вяртаецца. Маршрут адпавядае JSON, пакуль кліент чакае адпаведзь 101 Switching Protocols, і ніякая з сторон не робіць прыступаў.
Несправны варыянт пачынаецца з звычных імпортаў, укладаючы типы API з Next:
// pages/api/socket.ts — DOES NOT WORK
import { WebSocketServer } from "ws";
import type { NextApiRequest, NextApiResponse } from "next";
Пасля чаго маршрут стварае WebSocketServer і прыўяжвае каналобрабатар, які відтворяе паведамленні, але яго экспортаваны каналобрабатар проста вяртае статус у формате JSON.
const wss = new WebSocketServer({ noServer: true });wss.on("connection", (ws) => {
ws.on("message", (msg) => ws.send(`echo: ${msg}`));
});export default function handler(_req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ status: "ws server" });
}
Екземпляр wss існуе, і noServer: true ўсё ж такі правы настройкі. Адно, чаго не хапае, — гэта вызов функцыі handleUpgrade. Нічога не перадае сокет у ws, таму метод new WebSocket(...) браузера ніколі не завершае процэс узгоджэння.
Контроль над HTTP-серверам
Рашэння — перастаць спакоўвацца на тое, што гэта зробіць API-шлях, і кантролюваць HTTP-сервер безпасэчна. Next.js падтрымляе самастоялы сервер: вы самі ствараеце HTTP-сервер на Node, перадаеце звычныя запиты до працоўніка запитав Next і самі слухаеце за подыямі upgrade.
// server.ts
import { createServer } from "node:http";
import next from "next";
import { WebSocketServer, type WebSocket } from "ws";
import { parse } from "node:url";
Наведзены выконвальны скрыпт сервера выпрацоўвае чатыры задачы:
- Ён запускае Next у режыме разработкі або продакшну і отрымае стандартны працоўнік запитаў.
WebSocketServer з параметрам noServer: true і стежыць за адкрытымі сокетамі ў Set, выкарыстоўваючы ўмовы close і error для ўсунення яных.app.prepare() завершыцца, ён стварае HTTP-сэрвер, а у прыемніку upgrade пераглядае шлях: /api/ws пасылаецца да wss.handleUpgrade, а ўсё інша знишчаецца. У падсумку ён прыемліе запыты на PORT або 3000.const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();const wss = new WebSocketServer({ noServer: true });
const clients = new Set<WebSocket>();wss.on("connection", (ws) => {
clients.add(ws);
ws.on("message", (data) => {
const text = data.toString();
for (const client of clients) {
if (client.readyState === client.OPEN) client.send(text);
}
});
ws.on("close", () => clients.delete(ws));
ws.on("error", () => clients.delete(ws));
});app.prepare().then(() => {
const server = createServer((req, res) => handle(req, res, parse(req.url!, true))); server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url ?? "/", "http://localhost");
if (pathname === "/api/ws") {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
} else {
socket.destroy();
}
}); const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => console.log(`ready on http://localhost:${port}`));
});
Два элементы, якія вплываюць на дизайн
Першым є параметр noServer: true, які паведамляе модулю ws не ствараць сопрацоўнік для прыемліення запытав, таму што запыты апгрэйда буду падацца яму вручную.
Паўторна, перакантрольванне шляху файлу мае большое значэнне, ніж можа здавацца. Без яго кожны запит на апгрэйд, які падае на сервер, будзе перадаваны у функцыю handleUpgrade, укладаючы ў сябе таксама механізм замены модуляў у рэальны час, які викорыстоўвае Next пад час розработы. Шчытак захоўвае ваш канцэнтр пададчыка /api/ws аддзельным. Протыма, трэба памятаць, што ветка else знишчае ўсі іншыя запиты на апгрэйд; у залежнасці ад версіі Next.js гэта таксама можа перарваць з’ўязак HMR пад час розработы. Якщо функцыя гарячага перзавантажэння перестае працаваць, неабходна перадаць запиты на апгрэйд, якія не падходзяць, да Next замест таго, каб іх знишчаць (для ведамасці прыгледзіце раздзял пра спецыяльны сервер у чынных дакументах Next.js, дзе апісаны способы ўжыцтва гэтага).
Парадакт аргументаў у handleUpgrade лёгка паспяліць памыліцца. Як задокументавана для ws 8.18.3, ён выглядае так: handleUpgrade(request, socket, head, callback). Унутры калебэкаў вы самі выдаеце сигнал connection на серверы, таму працэсор wss.on("connection") функцыонуе абсолютна так жа, як і у стандартным серверы.
Кліентскі компонент
Браузерная частка спецыяльна простая. Як кліентскі компонент, яна пачынаецца з дырэктывы "use client" і неабходных для яе React хуків.
"use client";
import { useEffect, useState } from "react";
LiveFeed ачынае сокет на той жа пацёк, які захоўвае сервер, дадае кожна прыйнятая паведамленне да стану і адрасоўвае список.
export function LiveFeed() {
const [messages, setMessages] = useState<string[]>([]); useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${window.location.host}/api/ws`); ws.onmessage = (event) => {
setMessages((prev) => [...prev, event.data as string]);
}; return () => ws.close();
}, []); return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}
Два варыянты тут запобегаюць справжнім багам:
- Пратакт выведаецца з
window.location.protocol, а не зашыфраваным якws://. У рэжыме выпуску стораніца выдаўаецца через HTTPS, а браузеры блакуюць нешифраваныя з’язныws://з стораніцыhttps://; таму зашыфраванне паводзіцца да працяўкі толькі пасля размешчэння. - Эфект вяртае функцыю для чысткі, якая вызывае
ws.close(). У режыме разработкі React Strict Mode запускае эфекты два разы, таму без такой функціи ў кожны раз пад час запуску ствараўся бы дапаможны сокет.
Запуск сервера
Запустіце прыложэнне за допаможчыю tsx server.ts або вашага скомпіляванага эквівалента у рэжыме выпуску, а не next dev. Адмова ад команды разработкі без налагоджэння зарады керування HTTP-серверам — гэта справжня цена викорыстання натыўных WebSockets у такой наладзе.
Перакананне ў працэсе
Шырокі ручны пераконтроль, адконаны на Node 24 з ws 8.18.3, парадыржае кожны элемент:
- Калі падключены два кліенты, прыемлівка паведамлення на кшталт
hello from a, апрацоўванага першым, да другога адбываецца через цикл распрасовання. - Сокет, адкрытый на будзьм-якім паце, кроме
/api/ws, негайна збіваецца, а не застаёцца у стане очаквання, таму штоsocket.destroy()адразу ж яго закрывае, у працоўнасці з адметкай API-шляху. - Звычная HTTP-адметка на там самым сервере яшчэ і так вяртае свой тэл, што падтверджае, што прыемлівка запытаў у Next і апградэйш WebSocket-соедынення выкарыстоўваюць аднойчыны прыемлівач.
Дзе такой падход перестае ўзростаць
У адзінам процэсе Set кліентаў у памяці ёсць прыемным рашэнням, пакуль вы не запускаеце больш адной інстанцыі. Калі єсць два пады за лоад-балансэрам, прыемная на паде A ніколі не дасягае кліента, які падключены да пада B, таму што кожны процэс мае свой сабскіпт clients. У такім случае вам патрэбна шара публікацыі/падпискі, частаючы Redis, каб распрацаваць паведамленні між інстанцыямі, або кераваныя службы рэальнага часу. Чырпакавае інформацію пра камеры, стойкасць дадзеных і распространэнне паведамленняў можна знайсці ў дзе пісаюць пра проектаванне бэкендоў для чату ў рэальны час. Не стварайце такую систему з самага пачатку: адзін процэс Node можа кераваць многамі сокетамі адночасна, і багатыя прыкладнікі часта не вырастаюць за межы такога рашэння.
Абмежэнні хоставання
Адаптаваны сервер таксама выкарыстоўваецца не ў частыні модэлю развёрткі без сервера ад Next. Для WebSockets патрабуецца процес з довгачынным жыццёвым циклам, а не вызов функціі, як у Lambda. Якщо ваш хост запускае толькі функцыі без сервера, WebSockets не будуць працаваць там, незалежна ад таго, як вы напісаеце обробнік, таму пераканайцеся ва ўсьміх гэтых аспектах пры інвеставанні ў гэтую функцыю.
Ключовыя моменты
- Процес адкрывання з’ѐеднання WebSockets адбываецца пад час здачы сігналу
upgradeна сервере, якога API-маршрут ніколі не бачыць. - Адаптаваны сервер дазволяе направляць процесы адкрывання з’ѐеднання на протакол
wsза дапамою параметраўnoServer: trueіhandleUpgrade, тады як Next адпрацоўвае звычайны HTTP. - Захавайцеся ад несанкціонаваных процэсаў адкрывання з’ѐеднання па шляху, і чытка адначасова вядомы будзьце, што будзе з тымі процэсамі, якія не належаць вам.
- Выберайце протакол
wsабоwssна адповіднасць протаколу сторанкі, і завжды закрывайце з’ѐеднання пад час завершэння роботы.