Skip to content

Commit cc7bbb0

Browse files
authored
Merge pull request #467 from dahlia/token-required-perf
Optimize `tokenRequired` middleware performance
2 parents 74c7047 + d75233f commit cc7bbb0

20 files changed

Lines changed: 784 additions & 912 deletions

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ To be released.
9494
are removed; UnoCSS emits a single _src/public/uno.css_ whose
9595
URL is cache-busted by file mtime.
9696

97+
- Improved the performance of authenticated API requests by replacing the
98+
complex multi-table JOIN query in the `tokenRequired` middleware with a
99+
lightweight single-table lookup. Account owner data is now fetched on
100+
demand only by routes that actually need it, and requests that fail scope
101+
validation no longer touch the accounts table at all. [[#127], [#467]]
102+
97103
- Fixed a bug where incoming ActivityPub posts with timestamps more than
98104
12 hours in the future were accepted and stuck to the top of the
99105
federated timeline, enabling timeline manipulation via forged timestamps.
@@ -107,11 +113,13 @@ To be released.
107113

108114
[FEP-044f]: https://w3id.org/fep/044f
109115
[#67]: https://github.com/fedify-dev/hollo/issues/67
116+
[#127]: https://github.com/fedify-dev/hollo/issues/127
110117
[#457]: https://github.com/fedify-dev/hollo/pull/457
111118
[#458]: https://github.com/fedify-dev/hollo/pull/458
112119
[#459]: https://github.com/fedify-dev/hollo/pull/459
113120
[#460]: https://github.com/fedify-dev/hollo/pull/460
114121
[#466]: https://github.com/fedify-dev/hollo/pull/466
122+
[#467]: https://github.com/fedify-dev/hollo/pull/467
115123

116124

117125
Version 0.8.1

src/api/v1/accounts.ts

Lines changed: 28 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ import {
4242
import {
4343
scopeRequired,
4444
tokenRequired,
45-
type Variables,
45+
withAccountOwner,
46+
type AccountOwnerVariables,
4647
} from "../../oauth/middleware";
4748
import {
4849
type Account,
@@ -63,21 +64,16 @@ import {
6364
import { isUuid, type Uuid } from "../../uuid";
6465
import { timelineQuerySchema } from "./timelines";
6566

66-
const app = new Hono<{ Variables: Variables }>();
67+
const app = new Hono<{ Variables: AccountOwnerVariables }>();
6768
const allowedImageMimeTypes = ["image/gif", "image/jpeg", "image/png"];
6869

6970
app.get(
7071
"/verify_credentials",
7172
tokenRequired,
7273
scopeRequired(["read:accounts", "profile"]),
74+
withAccountOwner,
7375
async (c) => {
74-
const accountOwner = c.get("token").accountOwner;
75-
if (accountOwner == null) {
76-
return c.json(
77-
{ error: "This method requires an authenticated user" },
78-
422,
79-
);
80-
}
76+
const accountOwner = c.get("accountOwner");
8177
return c.json(serializeAccountOwner(accountOwner, c.req.url));
8278
},
8379
);
@@ -86,6 +82,7 @@ app.patch(
8682
"/update_credentials",
8783
tokenRequired,
8884
scopeRequired(["write:accounts"]),
85+
withAccountOwner,
8986
zValidator(
9087
"form",
9188
z.object({
@@ -117,13 +114,7 @@ app.patch(
117114
import("../../text"),
118115
]);
119116
const disk = drive.use();
120-
const owner = c.get("token").accountOwner;
121-
if (owner == null) {
122-
return c.json(
123-
{ error: "This method requires an authenticated user" },
124-
422,
125-
);
126-
}
117+
const owner = c.get("accountOwner");
127118
const account = owner.account;
128119
const form = c.req.valid("form");
129120
let avatarUrl: string | undefined;
@@ -266,14 +257,9 @@ app.get(
266257
"/relationships",
267258
tokenRequired,
268259
scopeRequired(["read:follows"]),
260+
withAccountOwner,
269261
async (c) => {
270-
const owner = c.get("token").accountOwner;
271-
if (owner == null) {
272-
return c.json(
273-
{ error: "This method requires an authenticated user" },
274-
422,
275-
);
276-
}
262+
const owner = c.get("accountOwner");
277263
const ids = (c.req.queries("id[]") ?? []).filter(isUuid);
278264
const accountList =
279265
ids.length > 0
@@ -315,7 +301,6 @@ app.get(
315301
}),
316302
),
317303
async (c) => {
318-
const owner = c.get("token")?.accountOwner;
319304
const query = c.req.valid("query");
320305
const acct = query.acct;
321306
let account:
@@ -338,15 +323,7 @@ app.get(
338323
return c.json({ error: "Record not found" }, 404);
339324
}
340325
const fedCtx = federation.createContext(c.req.raw, undefined);
341-
const options =
342-
owner == null
343-
? fedCtx
344-
: {
345-
contextLoader: fedCtx.contextLoader,
346-
documentLoader: await fedCtx.getDocumentLoader({
347-
username: owner.handle,
348-
}),
349-
};
326+
const options = fedCtx;
350327
const actor = await lookupObject(acct, options);
351328
if (!isActor(actor)) return c.json({ error: "Record not found" }, 404);
352329
const loaded = await persistAccount(db, actor, c.req.url, options);
@@ -448,14 +425,9 @@ app.get(
448425
"/familiar_followers",
449426
tokenRequired,
450427
scopeRequired(["read:follows"]),
428+
withAccountOwner,
451429
async (c) => {
452-
const owner = c.get("token").accountOwner;
453-
if (owner == null) {
454-
return c.json(
455-
{ error: "This method requires an authenticated user" },
456-
422,
457-
);
458-
}
430+
const owner = c.get("accountOwner");
459431
const ids: Uuid[] = (c.req.queries("id[]") ?? []).filter(isUuid);
460432
const result: {
461433
id: string;
@@ -514,6 +486,7 @@ app.get(
514486
"/:id/statuses",
515487
tokenRequired,
516488
scopeRequired(["read:statuses"]),
489+
withAccountOwner,
517490
zValidator(
518491
"query",
519492
timelineQuerySchema.extend({
@@ -527,13 +500,7 @@ app.get(
527500
async (c) => {
528501
const id = c.req.param("id");
529502
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
530-
const tokenOwner = c.get("token").accountOwner;
531-
if (tokenOwner == null) {
532-
return c.json(
533-
{ error: "This method requires an authenticated user" },
534-
422,
535-
);
536-
}
503+
const tokenOwner = c.get("accountOwner");
537504
const account = await db.query.accounts.findFirst({
538505
where: eq(accounts.id, id),
539506
with: {
@@ -701,16 +668,11 @@ app.post(
701668
"/:id/follow",
702669
tokenRequired,
703670
scopeRequired(["write:follows"]),
671+
withAccountOwner,
704672
async (c) => {
705673
const id = c.req.param("id");
706674
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
707-
const owner = c.get("token").accountOwner;
708-
if (owner == null) {
709-
return c.json(
710-
{ error: "This method requires an authenticated user" },
711-
422,
712-
);
713-
}
675+
const owner = c.get("accountOwner");
714676
const following = await db.query.accounts.findFirst({
715677
where: eq(accounts.id, id),
716678
with: { owner: true },
@@ -755,16 +717,11 @@ app.post(
755717
"/:id/unfollow",
756718
tokenRequired,
757719
scopeRequired(["write:follows"]),
720+
withAccountOwner,
758721
async (c) => {
759722
const id = c.req.param("id");
760723
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
761-
const owner = c.get("token").accountOwner;
762-
if (owner == null) {
763-
return c.json(
764-
{ error: "This method requires an authenticated user" },
765-
422,
766-
);
767-
}
724+
const owner = c.get("accountOwner");
768725
const following = await db.query.accounts.findFirst({
769726
where: eq(accounts.id, id),
770727
with: { owner: true },
@@ -841,16 +798,11 @@ app.get(
841798
"/:id/lists",
842799
tokenRequired,
843800
scopeRequired(["read:lists"]),
801+
withAccountOwner,
844802
async (c) => {
845803
const accountId = c.req.param("id");
846804
if (!isUuid(accountId)) return c.json({ error: "Record not found" }, 404);
847-
const owner = c.get("token").accountOwner;
848-
if (owner == null) {
849-
return c.json(
850-
{ error: "This method requires an authenticated user" },
851-
422,
852-
);
853-
}
805+
const owner = c.get("accountOwner");
854806
const listList = await db.query.lists.findMany({
855807
where: and(
856808
eq(lists.accountOwnerId, owner.id),
@@ -871,6 +823,7 @@ app.post(
871823
"/:id/mute",
872824
tokenRequired,
873825
scopeRequired(["write:mutes"]),
826+
withAccountOwner,
874827
zValidator(
875828
"json",
876829
z.object({
@@ -881,13 +834,7 @@ app.post(
881834
async (c) => {
882835
const id = c.req.param("id");
883836
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
884-
const owner = c.get("token").accountOwner;
885-
if (owner == null) {
886-
return c.json(
887-
{ error: "This method requires an authenticated user" },
888-
422,
889-
);
890-
}
837+
const owner = c.get("accountOwner");
891838
const { notifications, duration } = c.req.valid("json");
892839
const account = await db.query.accounts.findFirst({
893840
where: eq(accounts.id, id),
@@ -950,16 +897,11 @@ app.post(
950897
"/:id/unmute",
951898
tokenRequired,
952899
scopeRequired(["write:mutes"]),
900+
withAccountOwner,
953901
async (c) => {
954902
const id = c.req.param("id");
955903
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
956-
const owner = c.get("token").accountOwner;
957-
if (owner == null) {
958-
return c.json(
959-
{ error: "This method requires an authenticated user" },
960-
422,
961-
);
962-
}
904+
const owner = c.get("accountOwner");
963905
await db
964906
.delete(mutes)
965907
.where(and(eq(mutes.accountId, owner.id), eq(mutes.mutedAccountId, id)));
@@ -992,16 +934,11 @@ app.post(
992934
"/:id/block",
993935
tokenRequired,
994936
scopeRequired(["read:blocks"]),
937+
withAccountOwner,
995938
async (c) => {
996939
const id = c.req.param("id");
997940
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
998-
const owner = c.get("token").accountOwner;
999-
if (owner == null) {
1000-
return c.json(
1001-
{ error: "This method requires an authenticated user" },
1002-
422,
1003-
);
1004-
}
941+
const owner = c.get("accountOwner");
1005942
const acct = await db.query.accounts.findFirst({
1006943
where: eq(accounts.id, id),
1007944
with: { owner: true },
@@ -1038,16 +975,11 @@ app.post(
1038975
"/:id/unblock",
1039976
tokenRequired,
1040977
scopeRequired(["read:blocks"]),
978+
withAccountOwner,
1041979
async (c) => {
1042980
const id = c.req.param("id");
1043981
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
1044-
const owner = c.get("token").accountOwner;
1045-
if (owner == null) {
1046-
return c.json(
1047-
{ error: "This method requires an authenticated user" },
1048-
422,
1049-
);
1050-
}
982+
const owner = c.get("accountOwner");
1051983
const acct = await db.query.accounts.findFirst({
1052984
where: eq(accounts.id, id),
1053985
with: { owner: true },

src/api/v1/apps.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getLogger } from "@logtape/logtape";
2+
import { eq } from "drizzle-orm";
23
import { Hono } from "hono";
34
import { z } from "zod";
45

@@ -113,14 +114,17 @@ app.post("/", async (c) => {
113114

114115
app.get("/verify_credentials", tokenRequired, async (c) => {
115116
const token = c.get("token");
116-
const app = token.application;
117+
const application = await db.query.applications.findFirst({
118+
where: eq(applications.id, token.applicationId),
119+
});
120+
if (application == null) return c.json({ error: "invalid_token" }, 401);
117121
return c.json({
118-
id: app.id,
119-
name: app.name,
120-
website: app.website,
121-
scopes: app.scopes,
122-
redirect_uris: app.redirectUris,
123-
redirect_uri: app.redirectUris.join(" "),
122+
id: application.id,
123+
name: application.name,
124+
website: application.website,
125+
scopes: application.scopes,
126+
redirect_uris: application.redirectUris,
127+
redirect_uri: application.redirectUris.join(" "),
124128
});
125129
});
126130

0 commit comments

Comments
 (0)