tRPC: исправление чтения тела в Hono на Cloudflare Workers
Узнайте, как решить проблемы чтения тела запроса tRPC при использовании Hono в Cloudflare Workers. Руководство с исправлениями middleware и примерами.
tRPC Body Не Читается Корректно с Hono в Cloudflare Workers
У меня возникла проблема: tRPC не читает тело запроса корректно при использовании Hono в Cloudflare Workers. Несмотря на то, что тело присутствует в запросе, процедуры tRPC получают undefined в качестве входных данных.
Описание проблемы
Я пытаюсь интегрировать Cloudflare Workers, Hono и tRPC уже несколько часов. Я использую пакет @hono/trpc-server, который предоставляет middleware для запуска tRPC в Hono. Хотя я знаю, что такая интеграция возможна (есть пример с Cloudflare Workers и базой D1), я сталкиваюсь с тем, что tRPC вообще не читает тело запроса.
Пример процедуры
Ниже пример процедуры, которая не работает:
create: publicProcedure
.input(z.object({ title: z.string() }))
.mutation(async ({ ctx, input }) => {
console.error(input);
const quest = await ctx.db
.insertInto("quest")
.values(input)
.returningAll()
.executeTakeFirst();
return quest;
}),
Валидация падает до того, как процедура даже достигает мутации, и в формате ошибки видно, что входные данные undefined, когда запросы отправляются через Postman, хотя заголовок определённо присутствует в запросе.
Конфигурация сервера
Вот как я обслуживаю приложение:
const app = new Hono<{ Bindings: Bindings }>();
app.use(
"/trpc/*",
trpcServer({
router: appRouter,
createContext: (_, c) => createContext(c),
}),
);
app.use(
"/*",
cors({
origin: "http://localhost:5173",
credentials: true,
}),
);
export default app;
Настройка контекста
Вот моя конфигурация контекста:
import { createDb } from "./db";
import type { Bindings } from "./types";
import type { Context as HonoContext } from "hono";
export const createContext = (c: HonoContext<{ Bindings: Bindings }>) => ({
db: createDb(c.env.questboard),
});
export type Context = Awaited<ReturnType<typeof createContext>>;
Попытка отладки
Я попытался вывести тело запроса, чтобы убедиться, что оно присутствует:
app.use("/trpc/*", async (c, next) => {
console.log("Method:", c.req.method);
console.log("URL:", c.req.url);
console.log("Headers:", Object.fromEntries(c.req.raw.headers));
// Попытка прочитать тело
const clonedReq = c.req.raw.clone();
try {
const body = await clonedReq.text();
console.log("Raw body:", body);
} catch (e) {
console.log("Could not read body:", e);
}
return next();
});
Вывод выглядит так:
Method: POST
URL: http://localhost:8787/trpc/quest.create
Headers: {
accept: '*/*',
'accept-encoding': 'br, gzip',
'content-length': '36',
'content-type': 'application/json',
host: 'localhost:8787',
'postman-token': '5e10aea3-3ed3-4abd-8e2c-6158db447a21',
'user-agent': 'PostmanRuntime/7.49.1'
}
Raw body: {
"title": "Slay the Dragon"
}
Тело явно присутствует в запросе, но процедуры tRPC его не распознают.
Вопрос
Как можно настроить tRPC так, чтобы он корректно считывал и обрабатывал тело запроса при использовании Hono с Cloudflare Workers? Контекст и запросы к базе данных работают как надо, но входные данные не распознаются процедурами tRPC.
Проблема, с которой вы столкнулись, когда tRPC не читает тело запроса должным образом в Hono с Cloudflare Workers, является известной проблемой совместимости. Корень проблемы кроется в том, как чтение тела запроса обрабатывается между системой промежуточного ПО Hono и ожиданиями tRPC в среде Cloudflare Workers.
Согласно документации пакета Hono tRPC‑server, хотя эта интеграция предназначена для работы с Cloudflare Workers, могут возникать специфические проблемы с чтением тела запроса. В ваших отладочных данных видно, что сырое тело присутствует, но процедуры tRPC получают undefined в качестве входных данных, что указывает на проблему с обработкой промежуточного ПО.
Содержание
- Понимание проблемы чтения тела запроса
- Анализ причины
- Решения и обходные пути
- Исправления конфигурации промежуточного ПО
- Полный рабочий пример
- Лучшие практики
- Альтернативные подходы
Понимание проблемы чтения тела запроса
Проблема возникает из‑за того, что Cloudflare Workers имеют специфические ограничения при работе с запросами. Как отмечено в документации Hono для Cloudflare Workers, среда выполнения значительно отличается от Node.js, особенно в том, как читаются и кэшируются тела запросов.
В GitHub issue #1387 описана известная проблема, при которой тела запросов могут стать недоступными после определённых операций промежуточного ПО. Конкретно в вашем случае tRPC ожидает чтение тела запроса, но к моменту выполнения ваших процедур поток тела уже был потреблен или находится в несовместимом состоянии.
Анализ причины
Ваше отладочное промежуточное ПО корректно показывает, что сырое тело присутствует, но процедуры tRPC получают undefined. Это указывает на:
- Потребление потока запроса: Поток тела запроса может быть прочитан промежуточным ПО Hono до того, как tRPC сможет получить к нему доступ.
- Особенности Cloudflare Workers: Среда выполнения Cloudflare обрабатывает потоки запросов иначе, чем Node.js.
- Совместимость промежуточного ПО tRPC: Мидлвар
@hono/trpc-serverможет не корректно обрабатывать чтение тела запроса в этой среде.
Как показано в обсуждении на Stack Overflow, это распространённый паттерн, когда сырое тело читается, но обработанный ввод недоступен для процедур tRPC.
Решения и обходные пути
Решение 1: Корректное обработка тела запроса в промежуточном ПО
Измените ваше промежуточное ПО, чтобы гарантировать, что тело запроса будет корректно обработано до того, как оно достигнет tRPC:
app.use("/trpc/*", async (c, next) => {
// Обрабатываем только POST/PUT запросы с JSON
if (c.req.method === "POST" || c.req.method === "PUT") {
const contentType = c.req.header("content-type");
if (contentType?.includes("application/json")) {
try {
// Читаем и парсим тело заранее
const body = await c.req.json();
// Сохраняем распарсенное тело в контекст
c.set("parsedBody", body);
// Создаём новый запрос с распарсенным телом
const newRequest = new Request(c.req.url, {
method: c.req.method,
headers: c.req.headers,
body: JSON.stringify(body)
});
// Заменяем запрос
c.req = new Request(c.req.url, {
method: c.req.method,
headers: c.req.headers,
body: JSON.stringify(body)
});
} catch (e) {
console.error("Error parsing request body:", e);
}
}
}
await next();
});
Решение 2: Конфигурация промежуточного ПО tRPC
Обновите конфигурацию сервера tRPC, чтобы использовать пользовательский адаптер, который корректно обрабатывает чтение тела запроса:
import { createTRPCS } from "@trpc/server";
import { createTRPCContext } from "./context";
const trpc = createTRPCS();
app.use(
"/trpc/*",
trpcServer({
router: appRouter,
createContext: (opts) => {
const { c } = opts;
return createTRPCContext(c);
},
responseMeta: () => {
return {
headers: {
"Content-Type": "application/json",
},
};
},
})
);
Исправления конфигурации промежуточного ПО
Улучшенное промежуточное ПО для чтения тела
Создайте отдельное промежуточное ПО, которое будет обрабатывать чтение тела запроса:
const bodyReadingMiddleware = async (c: any, next: any) => {
// Клонируем запрос, чтобы прочитать тело без его потребления
const clonedReq = c.req.raw.clone();
// Читаем тело только для соответствующих запросов
if (clonedReq.method !== "GET" && clonedReq.method !== "HEAD") {
try {
const contentType = clonedReq.headers.get("content-type");
if (contentType?.includes("application/json")) {
const body = await clonedReq.text();
// Сохраняем сырое тело в контекст для tRPC
c.set("rawBody", body);
// Парсим и сохраняем распарсенное тело
const parsedBody = JSON.parse(body);
c.set("parsedBody", parsedBody);
}
} catch (error) {
console.error("Error reading request body:", error);
}
}
await next();
};
// Применяем промежуточное ПО до tRPC
app.use("/trpc/*", bodyReadingMiddleware);
Пользовательский обработчик tRPC
Создайте пользовательский обработчик tRPC, который будет работать с улучшенным промежуточным ПО:
const customTRPCHandler = trpcServer({
router: appRouter,
createContext: async ({ c }) => {
// Получаем распарсенное тело из нашего промежуточного ПО
const parsedBody = c.get("parsedBody");
return {
...createContext(c),
rawInput: parsedBody, // Передаём сырые данные в контекст
};
},
});
app.use("/trpc/*", customTRPCHandler);
Полный рабочий пример
Ниже приведена полная рабочая конфигурация, которая решает проблему чтения тела запроса:
import { Hono } from "hono";
import { trpcServer } from "@hono/trpc-server";
import { appRouter } from "./router";
import { createContext } from "./context";
import { cors } from "hono/cors";
const app = new Hono<{ Bindings: Bindings }>();
// Промежуточное ПО для обработки чтения тела запроса
app.use("/trpc/*", async (c, next) => {
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
const clonedReq = c.req.raw.clone();
try {
const contentType = clonedReq.headers.get("content-type");
if (contentType?.includes("application/json")) {
const body = await clonedReq.text();
const parsedBody = JSON.parse(body);
c.set("parsedBody", parsedBody);
}
} catch (error) {
console.error("Error reading request body:", error);
}
}
await next();
});
// Пользовательский обработчик tRPC, использующий распарсенное тело
app.use(
"/trpc/*",
trpcServer({
router: appRouter,
createContext: (opts) => {
const { c } = opts;
const parsedBody = c.get("parsedBody");
return {
...createContext(c),
// Передаём распарсенное тело в контекст для процедур
input: parsedBody,
};
},
})
);
// CORS‑промежуточное ПО
app.use(
"/*",
cors({
origin: "http://localhost:5173",
credentials: true,
})
);
export default app;
Лучшие практики
- Обработка тела запроса: Всегда обрабатывайте чтение тела запроса в промежуточном ПО до того, как оно достигнет tRPC.
- Обработка ошибок: Реализуйте надёжную обработку ошибок при парсинге JSON.
- Проверка Content‑Type: Проверяйте заголовок
content-typeперед попыткой парсинга JSON. - Особенности Cloudflare Workers: Помните, что Cloudflare Workers имеют другие характеристики обработки запросов.
- Тестирование: Тестируйте как с валидными, так и с невалидными телами запросов, чтобы убедиться в надёжности.
Альтернативные подходы
Если вышеуказанные решения не работают, рассмотрите следующие альтернативы:
Использование встроенного адаптера tRPC
import { createTRPC } from "@trpc/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const trpc = createTRPCS();
app.use(
"/trpc/*",
fetchRequestHandler({
endpoint: "/trpc",
req: c.req.raw,
router: appRouter,
createContext: (opts) => {
return createContext({ c: opts });
},
})
);
Пользовательский адаптер запроса
Создайте пользовательский адаптер, который корректно обрабатывает потоки запросов Cloudflare Workers:
const createTRPCAdapter = (router: any) => {
return async (c: any, next: any) => {
const url = new URL(c.req.url);
const path = url.pathname.replace("/trpc", "");
if (path.startsWith("/")) {
try {
const body = await c.req.json();
const result = await router._def.procedures[path.split(".")[0]]?._def.procedures[path.split(".")[1]]?.({
ctx: createContext(c),
input: body,
type: "mutation",
});
return c.json(result);
} catch (error) {
return c.json({ error: "Invalid request" }, 400);
}
}
await next();
};
};
app.use("/trpc/*", createTRPCAdapter(appRouter));
Ключ к решению этой проблемы — убедиться, что тело запроса корректно читается и становится доступным для процедур tRPC до их выполнения. Среда Cloudflare Workers требует особого внимания к обработке потоков запросов, а приведённые выше решения предлагают различные подходы, чтобы обеспечить совместимость между Hono, tRPC и Cloudflare Workers.