From 0d554384163315e443459fb1cf0f7716ee9ce9df Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:50:19 +0900 Subject: [PATCH 01/40] wip --- packages/backend/package.json | 2 ++ pnpm-lock.yaml | 22 ++++++++++++++++++++++ pnpm-workspace.yaml | 1 + 3 files changed, 25 insertions(+) diff --git a/packages/backend/package.json b/packages/backend/package.json index 3908243ba93..65b344d6928 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -62,6 +62,7 @@ "@fastify/http-proxy": "11.4.4", "@fastify/multipart": "10.0.0", "@fastify/static": "9.1.3", + "@hono/node-server": "2.0.0", "@kitajs/html": "4.2.13", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.5", @@ -100,6 +101,7 @@ "fluent-ffmpeg": "2.1.3", "form-data": "4.0.5", "got": "14.6.6", + "hono": "4.12.12", "hpagent": "1.2.0", "http-link-header": "1.1.3", "i18n": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9aa148f6c53..ea3f64cce17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: '@fastify/static': specifier: 9.1.3 version: 9.1.3 + '@hono/node-server': + specifier: 2.0.0 + version: 2.0.0(hono@4.12.12) '@kitajs/html': specifier: 4.2.13 version: 4.2.13 @@ -231,6 +234,9 @@ importers: got: specifier: 14.6.6 version: 14.6.6 + hono: + specifier: 4.12.12 + version: 4.12.12 hpagent: specifier: 1.2.0 version: 1.2.0 @@ -2289,6 +2295,12 @@ packages: '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@hono/node-server@2.0.0': + resolution: {integrity: sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -6600,6 +6612,10 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + hono@4.12.12: + resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + engines: {node: '>=16.9.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -11678,6 +11694,10 @@ snapshots: '@hexagon/base64@1.1.28': {} + '@hono/node-server@2.0.0(hono@4.12.12)': + dependencies: + hono: 4.12.12 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -16690,6 +16710,8 @@ snapshots: highlight.js@11.11.1: {} + hono@4.12.12: {} + hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4c94b582e05..d72220a7989 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,6 +35,7 @@ minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack minimumReleaseAgeExclude: - '@syuilo/aiscript' - '@typescript/native-preview*' + - '@hono/node-server' - fastify # 脆弱性対応。そのうち消す - '@fastify/express' # 脆弱性対応。そのうち消す - '@fastify/http-proxy' # 脆弱性対応。そのうち消す From d6ce1530656ce4448617b40136bf644b5e2f841b Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:58:31 +0900 Subject: [PATCH 02/40] migrate test-server --- packages/backend/test-server/entry.ts | 32 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts index 5abe8dd2967..a3cae1ca7f3 100644 --- a/packages/backend/test-server/entry.ts +++ b/packages/backend/test-server/entry.ts @@ -1,6 +1,7 @@ import { portToPid } from 'pid-port'; import fkill from 'fkill'; -import Fastify from 'fastify'; +import { Hono } from 'hono'; +import { serve } from '@hono/node-server'; import { NestFactory } from '@nestjs/core'; import { MainModule } from '@/MainModule.js'; import { ServerService } from '@/server/ServerService.js'; @@ -67,22 +68,24 @@ async function killTestServer() { * @param port */ async function startControllerEndpoints(port = config.port + 1000) { - const fastify = Fastify(); + const hono = new Hono(); - fastify.post<{ Body: { key?: string, value?: string } }>('/env', async (req, res) => { - console.log(req.body); - const key = req.body['key']; + hono.post('/env', async (c) => { + const req = await c.req.json<{ key?: string, value?: string }>(); + console.log(req); + const key = req['key']; if (!key) { - res.code(400).send({ success: false }); - return; + c.status(400); + return c.json({ success: false }); } - process.env[key] = req.body['value']; + process.env[key] = req['value']; - res.code(200).send({ success: true }); + c.status(200); + return c.json({ success: true }); }); - fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => { + hono.post('/env-reset', async (c) => { process.env = JSON.parse(originEnv); await serverService.dispose(); @@ -98,8 +101,13 @@ async function startControllerEndpoints(port = config.port + 1000) { serverService = app.get(ServerService); await serverService.launch(); - res.code(200).send({ success: true }); + c.status(200); + return c.json({ success: true }); }); - await fastify.listen({ port: port, host: 'localhost' }); + serve({ + fetch: hono.fetch, + port, + hostname: 'localhost', + }); } From ff7095fba6c073fa4deb930101172d916038a31c Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:05:14 +0900 Subject: [PATCH 03/40] migrate urlpreviewservice --- .../src/server/web/UrlPreviewService.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index bd1dbb430c9..c5e6ec38f5f 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -14,7 +14,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; import { MiMeta } from '@/models/Meta.js'; -import type { FastifyRequest, FastifyReply } from 'fastify'; +import type { Context } from 'hono'; @Injectable() export class UrlPreviewService { @@ -45,23 +45,23 @@ export class UrlPreviewService { @bindThis public async handle( - request: FastifyRequest<{ Querystring: { url: string; lang?: string; } }>, - reply: FastifyReply, + c: Context, ): Promise { - const url = request.query.url; + const url = c.req.query('url'); if (typeof url !== 'string') { - reply.code(400); + c.status(400); return; } - const lang = request.query.lang; - if (Array.isArray(lang)) { - reply.code(400); + const _lang = c.req.queries('lang') ?? []; + if (_lang.length > 1) { + c.status(400); return; } + const lang = _lang[0]; if (!this.meta.urlPreviewEnabled) { - reply.code(403); + c.status(403); return { error: new ApiError({ message: 'URL preview is disabled', @@ -94,14 +94,14 @@ export class UrlPreviewService { summary.thumbnail = this.wrap(summary.thumbnail); // Cache 1day - reply.header('Cache-Control', 'max-age=86400, immutable'); + c.res.headers.set('Cache-Control', 'max-age=86400, immutable'); return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); - reply.code(422); - reply.header('Cache-Control', 'max-age=86400, immutable'); + c.status(422); + c.res.headers.set('Cache-Control', 'max-age=86400, immutable'); return { error: new ApiError({ message: 'Failed to get preview', From 0a1af2a7f3ede2ad617ca2de3542524465c20a65 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:30:55 +0900 Subject: [PATCH 04/40] wip: migrate client server --- packages/backend/package.json | 2 - .../src/misc/hono-middleware-handlers.ts | 15 + packages/backend/src/misc/hono-vary.ts | 18 + packages/backend/src/server/ServerService.ts | 16 +- packages/backend/src/server/api/error.ts | 5 +- .../src/server/web/ClientServerService.ts | 613 +++++++++--------- .../src/server/web/HtmlTemplateService.ts | 7 - .../src/server/web/UrlPreviewService.ts | 48 +- .../src/server/web/views/base-embed.tsx | 37 +- .../backend/src/server/web/views/base.tsx | 38 +- .../backend/test-federation/tsconfig.json | 2 +- packages/backend/test-server/tsconfig.json | 2 +- packages/backend/test/tsconfig.json | 2 +- packages/backend/tsconfig.json | 5 +- pnpm-lock.yaml | 89 --- 15 files changed, 436 insertions(+), 463 deletions(-) create mode 100644 packages/backend/src/misc/hono-middleware-handlers.ts create mode 100644 packages/backend/src/misc/hono-vary.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 65b344d6928..ddd06acb20e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -63,7 +63,6 @@ "@fastify/multipart": "10.0.0", "@fastify/static": "9.1.3", "@hono/node-server": "2.0.0", - "@kitajs/html": "4.2.13", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.5", "@napi-rs/canvas": "0.1.97", @@ -159,7 +158,6 @@ "xev": "3.0.2" }, "devDependencies": { - "@kitajs/ts-html-plugin": "4.1.4", "@nestjs/platform-express": "11.1.19", "@rollup/plugin-esm-shim": "0.1.8", "@sentry/vue": "10.48.0", diff --git a/packages/backend/src/misc/hono-middleware-handlers.ts b/packages/backend/src/misc/hono-middleware-handlers.ts new file mode 100644 index 00000000000..e253c16d824 --- /dev/null +++ b/packages/backend/src/misc/hono-middleware-handlers.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { createMiddleware } from 'hono/factory'; + +export const handleRequestRedirectToOmitSearch = createMiddleware(async (c, next) => { + const index = c.req.url.indexOf('?'); + if (~index) { + return c.redirect(c.req.url.slice(0, index), 301); + } + await next(); + return; +}); diff --git a/packages/backend/src/misc/hono-vary.ts b/packages/backend/src/misc/hono-vary.ts new file mode 100644 index 00000000000..95c9bbbb38b --- /dev/null +++ b/packages/backend/src/misc/hono-vary.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Context as HonoContext } from 'hono'; + +export function vary(c: HonoContext, field: string) { + const varyHeader = c.res.headers.get('Vary'); + if (varyHeader != null) { + const fields = varyHeader.split(',').map((f) => f.trim()); + if (!fields.includes(field)) { + c.res.headers.set('Vary', `${varyHeader}, ${field}`); + } + } else { + c.res.headers.set('Vary', field); + } +} diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index ef9ac81f953..14216c57b40 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -7,6 +7,7 @@ import cluster from 'node:cluster'; import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { createAdaptorServer } from '@hono/node-server'; import Fastify, { type FastifyInstance } from 'fastify'; import fastifyStatic from '@fastify/static'; import fastifyRawBody from 'fastify-raw-body'; @@ -238,7 +239,20 @@ export class ServerService implements OnApplicationShutdown { } }); - fastify.register(this.clientServerService.createServer); + // fastify.register(this.clientServerService.createServer); + + // Create node-native clientserver + const clientHonoServer = this.clientServerService.createServer(); + const clientNodeServer = createAdaptorServer({ + fetch: clientHonoServer.fetch, + }); + // If hono matches, let it handle the request + fastify.addHook('onRequest', (request, reply) => new Promise((resolve) => { + clientNodeServer.on('close', () => { + resolve(); + }); + clientNodeServer.emit('request', request.raw, reply.raw); + })); this.streamingApiServerService.attach(fastify.server); diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 2f8322a5689..5ac31017996 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -2,15 +2,16 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import type { StatusCode } from 'hono/utils/http-status'; -type E = { message: string, code: string, id: string, kind?: 'client' | 'server' | 'permission', httpStatusCode?: number }; +type E = { message: string, code: string, id: string, kind?: 'client' | 'server' | 'permission', httpStatusCode?: StatusCode }; export class ApiError extends Error { public message: string; public code: string; public id: string; public kind: string; - public httpStatusCode?: number; + public httpStatusCode?: StatusCode; public info?: any; constructor(err?: E | null | undefined, info?: any | null | undefined) { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 9990d57f2ba..dce754ee3cc 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -9,9 +9,10 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import sharp from 'sharp'; import { In, IsNull } from 'typeorm'; -import fastifyStatic from '@fastify/static'; -import fastifyProxy from '@fastify/http-proxy'; -import vary from 'vary'; +import { Hono } from 'hono'; +import { every } from 'hono/combine'; +import { proxy } from 'hono/proxy'; +import { serveStatic } from '@hono/node-server/serve-static'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import * as Acct from '@/misc/acct.js'; @@ -34,13 +35,14 @@ import type { UserProfilesRepository, UsersRepository, } from '@/models/_.js'; -import type Logger from '@/logger.js'; -import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; +import { handleRequestRedirectToOmitSearch } from '@/misc/hono-middleware-handlers.js'; +import { vary } from '@/misc/hono-vary.js'; import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; +import { ApiError } from '@/server/api/error.js'; import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; import { ClientLoggerService } from './ClientLoggerService.js'; @@ -63,7 +65,7 @@ import { CliPage } from './views/cli.js'; import { FlushPage } from './views/flush.js'; import { ErrorPage } from './views/error.js'; -import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; +import type { Context as HonoContext } from 'hono'; @Injectable() export class ClientServerService { @@ -143,7 +145,7 @@ export class ClientServerService { } @bindThis - private async manifestHandler(reply: FastifyReply) { + private async manifestHandler(ctx: HonoContext) { let manifest = { // 空文字列の場合右辺を使いたいため // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -198,146 +200,125 @@ export class ClientServerService { ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride), }; - reply.header('Cache-Control', 'max-age=300'); - return (manifest); + ctx.header('Cache-Control', 'max-age=300'); + return ctx.json(manifest); } @bindThis - public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { + public createServer(): Hono { + const hono = new Hono(); const configUrl = new URL(this.config.url); - fastify.addHook('onRequest', (request, reply, done) => { - // クリックジャッキング防止のためiFrameの中に入れられないようにする - reply.header('X-Frame-Options', 'DENY'); - done(); + hono.use(async (ctx, next) => { + if ( + !ctx.req.path.startsWith('/embed/') && + !ctx.req.path.startsWith('/_info_card_') + ) { + // クリックジャッキング防止のためiFrameの中に入れられないようにする + ctx.header('X-Frame-Options', 'DENY'); + } + + await next(); }); //#region vite assets if (this.config.frontendEmbedManifestExists) { this.clientLoggerService.logger.info(`[ClientServerService] Using built frontend vite assets. ${this.frontendViteOut}`); - fastify.register((fastify, options, done) => { - fastify.register(fastifyStatic, { - root: this.frontendViteOut, - prefix: '/vite/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.register(fastifyStatic, { - root: this.frontendEmbedViteOut, - prefix: '/embed_vite/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - done(); - }); + hono.get('/vite/*', serveStatic({ + root: this.frontendViteOut, + }), every( + handleRequestRedirectToOmitSearch, + async (ctx, next) => { + ctx.header('Cache-Control', `max-age=${ms('30 days') / 1000}, immutable`); + await next(); + }, + )); + hono.get('/embed_vite/*', serveStatic({ + root: this.frontendEmbedViteOut, + }), every( + handleRequestRedirectToOmitSearch, + async (ctx, next) => { + ctx.header('Cache-Control', `max-age=${ms('30 days') / 1000}, immutable`); + await next(); + }, + )); } else { console.log('[ClientServerService] Proxying to Vite dev server.'); const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, ''); const port = (process.env.VITE_PORT ?? '5173'); - fastify.register(fastifyProxy, { - upstream: urlOriginWithoutPort + ':' + port, - prefix: '/vite', - rewritePrefix: '/vite', + hono.get('/vite/*', (ctx) => { + return proxy(`${urlOriginWithoutPort}:${port}${ctx.req.path}`); }); const embedPort = (process.env.EMBED_VITE_PORT ?? '5174'); - fastify.register(fastifyProxy, { - upstream: urlOriginWithoutPort + ':' + embedPort, - prefix: '/embed_vite', - rewritePrefix: '/embed_vite', + hono.get('/embed_vite/*', (ctx) => { + return proxy(`${urlOriginWithoutPort}:${embedPort}${ctx.req.path}`); }); } //#endregion //#region static assets - fastify.register(fastifyStatic, { + hono.get('/static-assets/*', serveStatic({ root: this.staticAssets, - prefix: '/static-assets/', - maxAge: ms('7 days'), - decorateReply: false, + }), async (ctx, next) => { + ctx.header('Cache-Control', `max-age=${ms('7 days') / 1000}`); + await next(); }); - fastify.register(fastifyStatic, { + hono.get('/client-assets/*', serveStatic({ root: this.clientAssets, - prefix: '/client-assets/', - maxAge: ms('7 days'), - decorateReply: false, + }), async (ctx, next) => { + ctx.header('Cache-Control', `max-age=${ms('7 days') / 1000}`); + await next() }); - fastify.register(fastifyStatic, { + hono.get('/assets/*', serveStatic({ root: this.assets, - prefix: '/assets/', - maxAge: ms('7 days'), - decorateReply: false, - }); - - fastify.register((fastify, options, done) => { - fastify.register(fastifyStatic, { - root: this.tarball, - prefix: '/tarball/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - done(); - }); - - fastify.get('/favicon.ico', async (request, reply) => { - return reply.sendFile('/favicon.ico', this.staticAssets); - }); - - fastify.get('/apple-touch-icon.png', async (request, reply) => { - return reply.sendFile('/apple-touch-icon.png', this.staticAssets); - }); - - fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { - const path = request.params.path; + }), async (ctx, next) => { + ctx.header('Cache-Control', `max-age=${ms('7 days') / 1000}`); + await next() + }); + + hono.get('/tarball/*', serveStatic({ + root: this.tarball, + }), every( + handleRequestRedirectToOmitSearch, + async (ctx, next) => { + ctx.header('Cache-Control', `max-age=${ms('30 days') / 1000}, immutable`); + await next(); + }, + )); - if (!path.match(/^[0-9a-f-]+\.png$/)) { - reply.code(404); - return; - } + hono.get('/favicon.ico', serveStatic({ + path: resolve(this.staticAssets, 'favicon.ico'), + })); - reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + hono.get('/apple-touch-icon.png', serveStatic({ + path: resolve(this.staticAssets, 'apple-touch-icon.png'), + })); - return reply.sendFile(path, this.fluentEmojisDir, { - maxAge: ms('30 days'), - }); + hono.get('/fluent-emoji/:filename{[0-9a-f-]+\\.png}', serveStatic({ + root: this.fluentEmojisDir, + }), async (ctx, next) => { + ctx.header('Cache-Control', `max-age=${ms('30 days') / 1000}`); + await next(); }); - fastify.get<{ Params: { path: string } }>('/twemoji/:path(.*)', async (request, reply) => { - const path = request.params.path; - - if (!path.match(/^[0-9a-f-]+\.svg$/)) { - reply.code(404); - return; - } - - reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - - return reply.sendFile(path, this.twemojiDir, { - maxAge: ms('30 days'), - }); + hono.get('/twemoji/:filename{[0-9a-f-]+\\.svg}', serveStatic({ + root: this.twemojiDir, + }), async (ctx, next) => { + ctx.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + + ctx.header('Cache-Control', `max-age=${ms('30 days') / 1000}`); + await next(); }); - fastify.get<{ Params: { path: string } }>('/twemoji-badge/:path(.*)', async (request, reply) => { - const path = request.params.path; - - if (!path.match(/^[0-9a-f-]+\.png$/)) { - reply.code(404); - return; - } - - const mask = await sharp( - `${this.twemojiDir}/${path.replace('.png', '')}.svg`, - { density: 1000 }, - ) + hono.get('/twemoji-badge/:filename{[0-9a-f-]+\\.png}', async (ctx) => { + const filename = ctx.req.param('filename'); + const path = resolve(this.twemojiDir, `${filename.replace('.png', '')}.svg`); + const mask = await sharp(path, { density: 1000 }) .resize(488, 488) .greyscale() .normalise() @@ -363,30 +344,26 @@ export class ClientServerService { .png() .toBuffer(); - reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - reply.header('Cache-Control', 'max-age=2592000'); - reply.header('Content-Type', 'image/png'); - return buffer; + ctx.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + ctx.header('Cache-Control', `max-age=${ms('30 days') / 1000}`); + ctx.header('Content-Type', 'image/png'); + return ctx.body(new Uint8Array(buffer)); }); // ServiceWorker - fastify.get('/sw.js', async (request, reply) => { - return await reply.sendFile('/sw.js', this.swAssets, { - maxAge: ms('10 minutes'), - }); - }); + hono.get('/sw.js', serveStatic({ + path: resolve(this.swAssets, 'sw.js'), + })); // Manifest - fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply)); + hono.get('/manifest.json', async (ctx) => await this.manifestHandler(ctx)); // Embed Javascript - fastify.get('/embed.js', async (request, reply) => { - return await reply.sendFile('/embed.js', this.staticAssets, { - maxAge: ms('1 day'), - }); - }); + hono.get('/embed.js', serveStatic({ + path: resolve(this.staticAssets, 'embed.js'), + })); - fastify.get('/robots.txt', async (request, reply) => { + hono.get('/robots.txt', async (ctx) => { const disallowedPaths = [ '/settings', '/admin', @@ -413,12 +390,11 @@ export class ClientServerService { content += 'Allow: /\n'; content += '\n# todo: sitemap\n'; - reply.header('Content-Type', 'text/plain; charset=utf-8'); - return await reply.send(content); + return ctx.text(content); }); // OpenSearch XML - fastify.get('/opensearch.xml', async (request, reply) => { + hono.get('/opensearch.xml', async (ctx) => { const name = this.meta.name ?? 'Misskey'; let content = ''; content += ''; @@ -429,15 +405,15 @@ export class ClientServerService { content += ``; content += ''; - reply.header('Content-Type', 'application/opensearchdescription+xml'); - return await reply.send(content); + ctx.header('Content-Type', 'application/opensearchdescription+xml'); + return ctx.body(content); }); //#endregion - const renderBase = async (reply: FastifyReply, data: Partial[0]> = {}) => { - reply.header('Cache-Control', 'public, max-age=30'); - return await HtmlTemplateService.replyHtml(reply, BasePage({ + const renderBase = async (ctx: HonoContext, data: Partial[0]> = {}) => { + ctx.header('Cache-Control', 'public, max-age=30'); + return ctx.html(BasePage({ img: this.meta.bannerUrl ?? undefined, title: this.meta.name ?? 'Misskey', desc: this.meta.description ?? undefined, @@ -446,8 +422,17 @@ export class ClientServerService { })); }; + const renderEmbedBase = async (ctx: HonoContext, data: Partial[0]> = {}) => { + ctx.header('Cache-Control', 'public, max-age=30'); + return ctx.html(BaseEmbed({ + title: this.meta.name ?? 'Misskey', + ...await this.htmlTemplateService.getCommonData(), + ...data, + })); + }; + // URL preview endpoint - fastify.get<{ Querystring: { url: string; lang: string; } }>('/url', (request, reply) => this.urlPreviewService.handle(request, reply)); + hono.get('/url', (c) => this.urlPreviewService.handle(c)); const getFeed = async (acct: string) => { const { username, host } = Acct.parse(acct); @@ -462,61 +447,67 @@ export class ClientServerService { }; // Atom - fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); + hono.get('/@:user.atom', async (ctx) => { + const user = ctx.req.param('user'); + if (user == null) return await renderBase(ctx); - const feed = await getFeed(request.params.user); + const feed = await getFeed(user); if (feed) { - reply.header('Content-Type', 'application/atom+xml; charset=utf-8'); - return feed.atom1(); + ctx.header('Content-Type', 'application/atom+xml; charset=utf-8'); + return ctx.body(feed.atom1()); } else { - reply.code(404); + ctx.status(404); return; } }); // RSS - fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); + hono.get('/@:user.rss', async (ctx) => { + const user = ctx.req.param('user'); + if (user == null) return await renderBase(ctx); - const feed = await getFeed(request.params.user); + const feed = await getFeed(user); if (feed) { - reply.header('Content-Type', 'application/rss+xml; charset=utf-8'); - return feed.rss2(); + ctx.header('Content-Type', 'application/rss+xml; charset=utf-8'); + return ctx.body(feed.rss2()); } else { - reply.code(404); + ctx.status(404); return; } }); // JSON - fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); + hono.get('/@:user.json', async (ctx) => { + const user = ctx.req.param('user'); + if (user == null) return await renderBase(ctx); - const feed = await getFeed(request.params.user); + const feed = await getFeed(user); if (feed) { - reply.header('Content-Type', 'application/json; charset=utf-8'); - return feed.json1(); + ctx.header('Content-Type', 'application/json; charset=utf-8'); + return ctx.json(feed.json1()); } else { - reply.code(404); + ctx.status(404); return; } }); //#region SSR // User - fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => { - const { username, host } = Acct.parse(request.params.user); + hono.get('/@:user/:sub?', async (ctx) => { + const userParam = ctx.req.param('user'); + if (userParam == null) return await renderBase(ctx); + + const { username, host } = Acct.parse(userParam); const user = await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, }); - vary(reply.raw, 'Accept'); + vary(ctx, 'Accept'); if ( user != null && ( @@ -526,10 +517,10 @@ export class ClientServerService { ) { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - reply.header('Cache-Control', 'public, max-age=15'); + ctx.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { - reply.header('X-Robots-Tag', 'noimageai'); - reply.header('X-Robots-Tag', 'noai'); + ctx.header('X-Robots-Tag', 'noimageai'); + ctx.header('X-Robots-Tag', 'noai'); } const _user = await this.userEntityService.pack(user, null, { @@ -537,10 +528,10 @@ export class ClientServerService { userProfile: profile, }); - return await HtmlTemplateService.replyHtml(reply, UserPage({ + return ctx.html(UserPage({ user: _user, profile, - sub: request.params.sub, + sub: ctx.req.param('sub'), ...await this.htmlTemplateService.getCommonData(), clientCtxJson: htmlSafeJsonStringify({ user: _user, @@ -549,34 +540,40 @@ export class ClientServerService { } else { // リモートユーザーなので // モデレータがAPI経由で参照可能にするために404にはしない - return await renderBase(reply); + return await renderBase(ctx); } }); - fastify.get<{ Params: { user: string; } }>('/users/:user', async (request, reply) => { + hono.get('/users/:user', async (ctx) => { + const userParam = ctx.req.param('user'); + if (userParam == null) return await renderBase(ctx); + const user = await this.usersRepository.findOneBy({ - id: request.params.user, + id: userParam, host: IsNull(), isSuspended: false, }); if (user == null) { - reply.code(404); + ctx.status(404); return; } - vary(reply.raw, 'Accept'); + vary(ctx, 'Accept'); - reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`); + return ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`); }); // Note - fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => { - vary(reply.raw, 'Accept'); + hono.get('/notes/:note', async (ctx) => { + vary(ctx, 'Accept'); + + const noteId = ctx.req.param('note'); + if (noteId == null) return await renderBase(ctx); const note = await this.notesRepository.findOne({ where: { - id: request.params.note, + id: noteId, visibility: In(['public', 'home']), }, relations: ['user', 'reply', 'renote'], @@ -591,12 +588,12 @@ export class ClientServerService { ) { const _note = await this.noteEntityService.pack(note); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); - reply.header('Cache-Control', 'public, max-age=15'); + ctx.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { - reply.header('X-Robots-Tag', 'noimageai'); - reply.header('X-Robots-Tag', 'noai'); + ctx.header('X-Robots-Tag', 'noimageai'); + ctx.header('X-Robots-Tag', 'noai'); } - return await HtmlTemplateService.replyHtml(reply, NotePage({ + return ctx.html(NotePage({ note: _note, profile, ...await this.htmlTemplateService.getCommonData(), @@ -605,13 +602,16 @@ export class ClientServerService { }), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); // Page - fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => { - const { username, host } = Acct.parse(request.params.user); + hono.get('/@:user/pages/:page', async (ctx) => { + const userParam = ctx.req.param('user'); + if (userParam == null) return await renderBase(ctx); + + const { username, host } = Acct.parse(userParam); const user = await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: host ?? IsNull(), @@ -620,7 +620,7 @@ export class ClientServerService { if (user == null) return; const page = await this.pagesRepository.findOneBy({ - name: request.params.page, + name: ctx.req.param('page'), userId: user.id, }); @@ -628,63 +628,69 @@ export class ClientServerService { const _page = await this.pageEntityService.pack(page); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId }); if (['public'].includes(page.visibility)) { - reply.header('Cache-Control', 'public, max-age=15'); + ctx.header('Cache-Control', 'public, max-age=15'); } else { - reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); + ctx.header('Cache-Control', 'private, max-age=0, must-revalidate'); } if (profile.preventAiLearning) { - reply.header('X-Robots-Tag', 'noimageai'); - reply.header('X-Robots-Tag', 'noai'); + ctx.header('X-Robots-Tag', 'noimageai'); + ctx.header('X-Robots-Tag', 'noai'); } - return await HtmlTemplateService.replyHtml(reply, PagePage({ + return ctx.html(PagePage({ page: _page, profile, ...await this.htmlTemplateService.getCommonData(), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); // Flash - fastify.get<{ Params: { id: string; } }>('/play/:id', async (request, reply) => { + hono.get('/play/:id', async (ctx) => { + const flashId = ctx.req.param('id'); + if (flashId == null) return await renderBase(ctx); + const flash = await this.flashsRepository.findOneBy({ - id: request.params.id, + id: flashId, }); if (flash) { const _flash = await this.flashEntityService.pack(flash); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); - reply.header('Cache-Control', 'public, max-age=15'); + ctx.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { - reply.header('X-Robots-Tag', 'noimageai'); - reply.header('X-Robots-Tag', 'noai'); + ctx.header('X-Robots-Tag', 'noimageai'); + ctx.header('X-Robots-Tag', 'noai'); } - return await HtmlTemplateService.replyHtml(reply, FlashPage({ + return ctx.html(FlashPage({ flash: _flash, profile, ...await this.htmlTemplateService.getCommonData(), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); // Clip - fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => { + hono.get('/clips/:clip', async (ctx) => { + const clipId = ctx.req.param('clip'); + if (clipId == null) return await renderBase(ctx); + const clip = await this.clipsRepository.findOneBy({ - id: request.params.clip, + id: clipId, }); if (clip && clip.isPublic) { const _clip = await this.clipEntityService.pack(clip); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); - reply.header('Cache-Control', 'public, max-age=15'); + ctx.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { - reply.header('X-Robots-Tag', 'noimageai'); - reply.header('X-Robots-Tag', 'noai'); + ctx.header('X-Robots-Tag', 'noimageai'); + ctx.header('X-Robots-Tag', 'noai'); } - return await HtmlTemplateService.replyHtml(reply, ClipPage({ + return ctx.html(ClipPage({ clip: _clip, profile, ...await this.htmlTemplateService.getCommonData(), @@ -693,106 +699,119 @@ export class ClientServerService { }), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); // Gallery post - fastify.get<{ Params: { post: string; } }>('/gallery/:post', async (request, reply) => { - const post = await this.galleryPostsRepository.findOneBy({ id: request.params.post }); + hono.get('/gallery/:post', async (ctx) => { + const postId = ctx.req.param('post'); + if (postId == null) return await renderBase(ctx); + + const post = await this.galleryPostsRepository.findOneBy({ id: postId }); if (post) { const _post = await this.galleryPostEntityService.pack(post); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); - reply.header('Cache-Control', 'public, max-age=15'); + ctx.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { - reply.header('X-Robots-Tag', 'noimageai'); - reply.header('X-Robots-Tag', 'noai'); + ctx.header('X-Robots-Tag', 'noimageai'); + ctx.header('X-Robots-Tag', 'noai'); } - return await HtmlTemplateService.replyHtml(reply, GalleryPostPage({ + return ctx.html(GalleryPostPage({ galleryPost: _post, profile, ...await this.htmlTemplateService.getCommonData(), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); // Channel - fastify.get<{ Params: { channel: string; } }>('/channels/:channel', async (request, reply) => { + hono.get('/channels/:channel', async (ctx) => { + const channelId = ctx.req.param('channel'); + if (channelId == null) return await renderBase(ctx); + const channel = await this.channelsRepository.findOneBy({ - id: request.params.channel, + id: channelId, }); if (channel) { const _channel = await this.channelEntityService.pack(channel); - reply.header('Cache-Control', 'public, max-age=15'); - return await HtmlTemplateService.replyHtml(reply, ChannelPage({ + ctx.header('Cache-Control', 'public, max-age=15'); + return ctx.html(ChannelPage({ channel: _channel, ...await this.htmlTemplateService.getCommonData(), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); // Reversi game - fastify.get<{ Params: { game: string; } }>('/reversi/g/:game', async (request, reply) => { + hono.get('/reversi/g/:game', async (ctx) => { + const gameId = ctx.req.param('game'); + if (gameId == null) return await renderBase(ctx); + const game = await this.reversiGamesRepository.findOneBy({ - id: request.params.game, + id: gameId, }); if (game) { const _game = await this.reversiGameEntityService.packDetail(game); - reply.header('Cache-Control', 'public, max-age=3600'); - return await HtmlTemplateService.replyHtml(reply, ReversiGamePage({ + ctx.header('Cache-Control', 'public, max-age=3600'); + return ctx.html(ReversiGamePage({ reversiGame: _game, ...await this.htmlTemplateService.getCommonData(), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); // 個別お知らせページ - fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => { + hono.get('/announcements/:announcementId', async (ctx) => { + const announcementId = ctx.req.param('announcementId'); + if (announcementId == null) return await renderBase(ctx); + const announcement = await this.announcementsRepository.findOneBy({ - id: request.params.announcementId, + id: announcementId, userId: IsNull(), }); if (announcement) { const _announcement = await this.announcementEntityService.pack(announcement); - reply.header('Cache-Control', 'public, max-age=3600'); - return await HtmlTemplateService.replyHtml(reply, AnnouncementPage({ + ctx.header('Cache-Control', 'public, max-age=3600'); + return ctx.html(AnnouncementPage({ announcement: _announcement, ...await this.htmlTemplateService.getCommonData(), })); } else { - return await renderBase(reply); + return await renderBase(ctx); } }); //#endregion //#region noindex pages // Tags - fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => { - return await renderBase(reply, { noindex: true }); + hono.get('/tags/:tag', async (ctx) => { + return await renderBase(ctx, { noindex: true }); }); // User with Tags - fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => { - return await renderBase(reply, { noindex: true }); + hono.get('/user-tags/:tag', async (ctx) => { + return await renderBase(ctx, { noindex: true }); }); //#endregion //#region embed pages - fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); + hono.get('/embed/user-timeline/:user', async (ctx) => { + const userId = ctx.req.param('user'); + if (userId == null) return await renderEmbedBase(ctx); const user = await this.usersRepository.findOneBy({ - id: request.params.user, + id: userId, }); if (user == null) return; @@ -800,22 +819,21 @@ export class ClientServerService { const _user = await this.userEntityService.pack(user); - reply.header('Cache-Control', 'public, max-age=3600'); - return await HtmlTemplateService.replyHtml(reply, BaseEmbed({ - title: this.meta.name ?? 'Misskey', - ...await this.htmlTemplateService.getCommonData(), + ctx.header('Cache-Control', 'public, max-age=3600'); + return await renderEmbedBase(ctx, { embedCtxJson: htmlSafeJsonStringify({ user: _user, }), - })); + }); }); - fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); + hono.get('/embed/notes/:note', async (ctx) => { + const noteId = ctx.req.param('note'); + if (noteId == null) return await renderEmbedBase(ctx); const note = await this.notesRepository.findOne({ where: { - id: request.params.note, + id: noteId, }, relations: ['user', 'reply', 'renote'], }); @@ -826,51 +844,41 @@ export class ClientServerService { const _note = await this.noteEntityService.pack(note, null, { detail: true }); - reply.header('Cache-Control', 'public, max-age=3600'); - return await HtmlTemplateService.replyHtml(reply, BaseEmbed({ - title: this.meta.name ?? 'Misskey', - ...await this.htmlTemplateService.getCommonData(), + ctx.header('Cache-Control', 'public, max-age=3600'); + return await renderEmbedBase(ctx, { embedCtxJson: htmlSafeJsonStringify({ note: _note, }), - })); + }); }); - fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); + hono.get('/embed/clips/:clip', async (ctx) => { + const clipId = ctx.req.param('clip'); + if (clipId == null) return await renderEmbedBase(ctx); const clip = await this.clipsRepository.findOneBy({ - id: request.params.clip, + id: clipId, }); if (clip == null) return; const _clip = await this.clipEntityService.pack(clip); - reply.header('Cache-Control', 'public, max-age=3600'); - return await HtmlTemplateService.replyHtml(reply, BaseEmbed({ - title: this.meta.name ?? 'Misskey', - ...await this.htmlTemplateService.getCommonData(), + ctx.header('Cache-Control', 'public, max-age=3600'); + return await renderEmbedBase(ctx, { embedCtxJson: htmlSafeJsonStringify({ clip: _clip, }), - })); + }); }); - fastify.get('/embed/*', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); - - reply.header('Cache-Control', 'public, max-age=3600'); - return await HtmlTemplateService.replyHtml(reply, BaseEmbed({ - title: this.meta.name ?? 'Misskey', - ...await this.htmlTemplateService.getCommonData(), - })); + hono.get('/embed/*', async (ctx) => { + ctx.header('Cache-Control', 'public, max-age=3600'); + return await renderEmbedBase(ctx); }); - fastify.get('/_info_card_', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); - - return await HtmlTemplateService.replyHtml(reply, InfoCardPage({ + hono.get('/_info_card_', async (ctx) => { + return ctx.html(InfoCardPage({ version: this.config.version, config: this.config, meta: this.meta, @@ -878,23 +886,24 @@ export class ClientServerService { }); //#endregion - fastify.get('/bios', async (request, reply) => { - return await HtmlTemplateService.replyHtml(reply, BiosPage({ + hono.get('/bios', async (ctx) => { + return ctx.html(BiosPage({ version: this.config.version, })); }); - fastify.get('/cli', async (request, reply) => { - return await HtmlTemplateService.replyHtml(reply, CliPage({ + hono.get('/cli', async (ctx) => { + return ctx.html(CliPage({ version: this.config.version, })); }); - fastify.get('/flush', async (request, reply) => { + hono.get('/flush', async (ctx) => { let sendHeader = true; - if (request.headers['origin']) { - const originURL = new URL(request.headers['origin']); + const originHeader = ctx.req.header('Origin'); + if (originHeader != null) { + const originURL = new URL(originHeader); if (originURL.protocol !== 'https:') { // Clear-Site-Data only supports https sendHeader = false; } @@ -904,41 +913,59 @@ export class ClientServerService { } if (sendHeader) { - reply.header('Clear-Site-Data', '"*"'); + ctx.header('Clear-Site-Data', '"*"'); } - reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60'); - return await HtmlTemplateService.replyHtml(reply, FlushPage()); + ctx.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60'); + return ctx.html(FlushPage()); }); // streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる - fastify.get('/streaming', async (request, reply) => { - reply.code(503); - reply.header('Cache-Control', 'private, max-age=0'); + hono.get('/streaming', async (ctx) => { + ctx.status(503); + ctx.header('Cache-Control', 'private, max-age=0'); + return; }); // Render base html for all requests - fastify.get('*', async (request, reply) => { - return await renderBase(reply); - }); - - fastify.setErrorHandler(async (error, request, reply) => { - const errId = randomUUID(); - this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, { - path: request.routeOptions.url, - params: request.params, - query: request.query, - code: error.name, - stack: error.stack, - id: errId, - }); - reply.code(500); - reply.header('Cache-Control', 'max-age=10, must-revalidate'); - return await HtmlTemplateService.replyHtml(reply, ErrorPage({ - code: error.code, - id: errId, - })); + hono.get('*', async (ctx) => { + return await renderBase(ctx); + }); + + hono.onError(async (err, ctx) => { + // ClientServerでも、RSS・JSONなどのエンドポイントでApiErrorが発生することがある + if (err instanceof ApiError) { + ctx.status(err.httpStatusCode ?? 500); + ctx.header('Cache-Control', 'max-age=10, must-revalidate'); + + // Must be synced with ApiCallService.send + return ctx.json({ + error: { + message: err.message, + code: err.code, + id: err.id, + kind: err.kind, + ...(err.info ? { info: err.info } : {}), + }, + }); + } else { + const errId = randomUUID(); + this.clientLoggerService.logger.error(`Internal error occurred in ${ctx.req.path}: ${err.message}`, { + path: ctx.req.path, + params: ctx.req.param(), + query: ctx.req.query(), + code: err.name, + stack: err.stack, + id: errId, + }); + ctx.status(500); + ctx.header('Cache-Control', 'max-age=10, must-revalidate'); + return ctx.html(ErrorPage({ + code: err.name, + id: errId, + })); + } }); - done(); + return hono; } } diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts index 2859b2b9852..1e061b0190f 100644 --- a/packages/backend/src/server/web/HtmlTemplateService.ts +++ b/packages/backend/src/server/web/HtmlTemplateService.ts @@ -11,7 +11,6 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js'; import { MetaEntityService } from '@/core/entities/MetaEntityService.js'; -import type { FastifyReply } from 'fastify'; import type { Manifest } from 'vite'; import type { Config } from '@/config.js'; import type { MiMeta } from '@/models/Meta.js'; @@ -176,10 +175,4 @@ export class HtmlTemplateService { frontendEmbedBootloaderCss: this.frontendEmbedBootloaderCss, }; } - - public static async replyHtml(reply: FastifyReply, html: string | Promise) { - reply.header('Content-Type', 'text/html; charset=utf-8'); - const _html = await html; - return reply.send(_html); - } } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index c5e6ec38f5f..784280942da 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -14,7 +14,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; import { MiMeta } from '@/models/Meta.js'; -import type { Context } from 'hono'; +import type { Context as HonoContext } from 'hono'; @Injectable() export class UrlPreviewService { @@ -45,30 +45,29 @@ export class UrlPreviewService { @bindThis public async handle( - c: Context, - ): Promise { - const url = c.req.query('url'); + ctx: HonoContext, + ) { + const url = ctx.req.query('url'); if (typeof url !== 'string') { - c.status(400); + ctx.status(400); return; } - const _lang = c.req.queries('lang') ?? []; + const _lang = ctx.req.queries('lang') ?? []; if (_lang.length > 1) { - c.status(400); + ctx.status(400); return; } const lang = _lang[0]; if (!this.meta.urlPreviewEnabled) { - c.status(403); - return { - error: new ApiError({ - message: 'URL preview is disabled', - code: 'URL_PREVIEW_DISABLED', - id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8', - }), - }; + ctx.status(403); + throw new ApiError({ + message: 'URL preview is disabled', + code: 'URL_PREVIEW_DISABLED', + id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8', + httpStatusCode: 403, + }); } this.logger.info(this.meta.urlPreviewSummaryProxyUrl @@ -94,21 +93,18 @@ export class UrlPreviewService { summary.thumbnail = this.wrap(summary.thumbnail); // Cache 1day - c.res.headers.set('Cache-Control', 'max-age=86400, immutable'); + ctx.res.headers.set('Cache-Control', 'max-age=86400, immutable'); - return summary; + return ctx.json(summary); } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); - c.status(422); - c.res.headers.set('Cache-Control', 'max-age=86400, immutable'); - return { - error: new ApiError({ - message: 'Failed to get preview', - code: 'URL_PREVIEW_FAILED', - id: '09d01cb5-53b9-4856-82e5-38a50c290a3b', - }), - }; + throw new ApiError({ + message: 'Failed to get preview', + code: 'URL_PREVIEW_FAILED', + id: '09d01cb5-53b9-4856-82e5-38a50c290a3b', + httpStatusCode: 422, + }); } } diff --git a/packages/backend/src/server/web/views/base-embed.tsx b/packages/backend/src/server/web/views/base-embed.tsx index a656bb28a71..8318d8a8f18 100644 --- a/packages/backend/src/server/web/views/base-embed.tsx +++ b/packages/backend/src/server/web/views/base-embed.tsx @@ -3,10 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { raw } from 'hono/utils/html'; +import type { PropsWithChildren, Child } from 'hono/jsx'; import { comment } from '@/server/web/views/_.js'; import type { CommonProps } from '@/server/web/views/_.js'; import { Splash } from '@/server/web/views/_splash.js'; -import type { PropsWithChildren, Children } from '@kitajs/html'; export function BaseEmbed(props: PropsWithChildren>) { const now = Date.now(); - // 変数名をsafeで始めることでエラーをスキップ - const safeMetaJson = props.metaJson; - const safeEmbedCtxJson = props.embedCtxJson; + const metaJson = props.metaJson; + const embedCtxJson = props.embedCtxJson; + + const doctypeTag = raw(''); + const commentTag = raw(comment); return ( <> - {''} - {comment} + {doctypeTag} + {commentTag} @@ -52,24 +55,22 @@ export function BaseEmbed(props: PropsWithChildren ))} - {props.titleSlot ?? {props.title || 'Misskey'}} + {props.titleSlot ?? {props.title || 'Misskey'}} {props.metaSlot} - {props.frontendEmbedBootloaderCss != null ? : } + {props.frontendEmbedBootloaderCss != null ? : } - + - {safeMetaJson != null ? : null} - {safeEmbedCtxJson != null ? : null} + {metaJson != null ? : null} + {embedCtxJson != null ? : null} - {props.frontendEmbedBootloaderJs != null ? : } + {props.frontendEmbedBootloaderJs != null ? : }