Refactor/fix error handling, add query timeouts

This commit is contained in:
Râu Cao 2024-10-25 14:25:41 +02:00
parent ec7c775e25
commit 5f5f024ae7
Signed by: raucao
GPG Key ID: 37036C356E56CC51
12 changed files with 168 additions and 113 deletions

View File

@ -45,6 +45,7 @@ const config = {
password: Deno.env.get("LDAP_PASSWORD"), password: Deno.env.get("LDAP_PASSWORD"),
searchDN: Deno.env.get("LDAP_SEARCH_DN"), searchDN: Deno.env.get("LDAP_SEARCH_DN"),
}, },
query_timeout: parseInt(Deno.env.get("RELAY_TIMEOUT_MS") || "5000"),
njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me", njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me",
}; };

20
handlers/errors.ts Normal file
View File

@ -0,0 +1,20 @@
import { Context } from "@oak/oak";
import { errorPageHtml } from "../html.ts";
export const notFoundHandler = function (ctx: Context) {
const html = errorPageHtml(404, "Not found");
ctx.response.body = html;
ctx.response.status = 404;
};
export const badGatewayHandler = function (ctx: Context) {
const html = errorPageHtml(502, "Bad gateway");
ctx.response.body = html;
ctx.response.status = 502;
};
export const internalServerErrorHandler = function (ctx: Context) {
const html = errorPageHtml(500, "Internal server error");
ctx.response.body = html;
ctx.response.status = 500;
};

View File

@ -1,22 +1,25 @@
import { Context } from "@oak/oak"; import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools"; import { nip19 } from "@nostr/tools";
import { lookupUsernameByPubkey } from "../directory.ts"; import { lookupUsernameByPubkey } from "../directory.ts";
import notFoundHandler from "../handlers/not-found.ts"; import { notFoundHandler } from "../handlers/errors.ts";
const naddrHandler = async function (ctx: Context) { const naddrHandler = async function (ctx: Context) {
const naddr = ctx.state.path; const naddr = ctx.state.path;
let data: nip19.AddressPointer;
try { try {
const data = nip19.decode(naddr).data as nip19.AddressPointer; data = nip19.decode(naddr).data as nip19.AddressPointer;
const username = await lookupUsernameByPubkey(data.pubkey);
if (username && data.identifier) {
ctx.response.redirect(`/@${username}/${data.identifier}`);
} else {
notFoundHandler(ctx);
}
} catch (_e) { } catch (_e) {
notFoundHandler(ctx); notFoundHandler(ctx);
return;
}
const username = await lookupUsernameByPubkey(data.pubkey);
if (username && data.identifier) {
ctx.response.redirect(`/@${username}/${data.identifier}`);
} else {
notFoundHandler(ctx);
} }
}; };

View File

@ -1,10 +0,0 @@
import { Context } from "@oak/oak";
import { errorPageHtml } from "../html.ts";
const notFoundHandler = function (ctx: Context) {
const html = errorPageHtml(404, "Not found");
ctx.response.body = html;
ctx.response.status = 404;
};
export default notFoundHandler;

View File

@ -1,22 +1,26 @@
import { Context } from "@oak/oak"; import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools"; import { nip19 } from "@nostr/tools";
import { lookupUsernameByPubkey } from "../directory.ts"; import { lookupUsernameByPubkey } from "../directory.ts";
import notFoundHandler from "../handlers/not-found.ts"; import { notFoundHandler } from "../handlers/errors.ts";
const nprofileHandler = async function (ctx: Context) { const nprofileHandler = async function (ctx: Context) {
const nprofile = ctx.state.path; const nprofile = ctx.state.path;
let data: nip19.ProfilePointer;
try { try {
const data = nip19.decode(nprofile).data as nip19.ProfilePointer; data = nip19.decode(nprofile).data as nip19.ProfilePointer;
const username = await lookupUsernameByPubkey(data.pubkey); console.log(data);
if (username) {
ctx.response.redirect(`/@${username}`);
} else {
notFoundHandler(ctx);
}
} catch (_e) { } catch (_e) {
notFoundHandler(ctx); notFoundHandler(ctx);
return;
}
const username = await lookupUsernameByPubkey(data.pubkey);
if (username) {
ctx.response.redirect(`/@${username}`);
} else {
notFoundHandler(ctx);
} }
}; };

View File

@ -1,22 +1,25 @@
import { Context } from "@oak/oak"; import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools"; import { nip19 } from "@nostr/tools";
import { lookupUsernameByPubkey } from "../directory.ts"; import { lookupUsernameByPubkey } from "../directory.ts";
import notFoundHandler from "../handlers/not-found.ts"; import { notFoundHandler } from "../handlers/errors.ts";
const npubHandler = async function (ctx: Context) { const npubHandler = async function (ctx: Context) {
const npub = ctx.state.path; const npub = ctx.state.path;
let pubkey: string;
try { try {
const pubkey = nip19.decode(npub).data as string; pubkey = nip19.decode(npub).data as string;
const username = await lookupUsernameByPubkey(pubkey);
if (username) {
ctx.response.redirect(`/@${username}`);
} else {
notFoundHandler(ctx);
}
} catch (_e) { } catch (_e) {
notFoundHandler(ctx); notFoundHandler(ctx);
return;
}
const username = await lookupUsernameByPubkey(pubkey);
if (username) {
ctx.response.redirect(`/@${username}`);
} else {
notFoundHandler(ctx);
} }
}; };

View File

@ -4,7 +4,7 @@ import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
import { profileAtomFeed } from "../feeds.ts"; import { profileAtomFeed } from "../feeds.ts";
import Article from "../models/article.ts"; import Article from "../models/article.ts";
import Profile from "../models/profile.ts"; import Profile from "../models/profile.ts";
import notFoundHandler from "../handlers/not-found.ts"; import { notFoundHandler } from "../handlers/errors.ts";
const userAtomFeedHandler = async function (ctx: Context) { const userAtomFeedHandler = async function (ctx: Context) {
const username = ctx.state.username; const username = ctx.state.username;
@ -15,26 +15,22 @@ const userAtomFeedHandler = async function (ctx: Context) {
return; return;
} }
try { const profileEvent = await fetchProfileEvent(pubkey);
const profileEvent = await fetchProfileEvent(pubkey);
if (profileEvent) { if (profileEvent) {
const profile = new Profile(profileEvent, username); const profile = new Profile(profileEvent, username);
if (profile.nip05) { if (profile.nip05) {
const articleEvents = await fetchArticlesByAuthor(pubkey); const articleEvents = await fetchArticlesByAuthor(pubkey);
const articles = articleEvents.map((a) => new Article(a)); const articles = articleEvents.map((a) => new Article(a));
const atom = profileAtomFeed(profile, articles); const atom = profileAtomFeed(profile, articles);
ctx.response.headers.set("Content-Type", "application/atom+xml"); ctx.response.headers.set("Content-Type", "application/atom+xml");
ctx.response.body = atom; ctx.response.body = atom;
return; return;
}
} }
notFoundHandler(ctx);
} catch (_e) {
notFoundHandler(ctx);
} }
notFoundHandler(ctx);
}; };
export default userAtomFeedHandler; export default userAtomFeedHandler;

View File

@ -4,7 +4,7 @@ import { fetchProfileEvent, fetchReplaceableEvent } from "../nostr.ts";
import Article from "../models/article.ts"; import Article from "../models/article.ts";
import Profile from "../models/profile.ts"; import Profile from "../models/profile.ts";
import { articleHtml } from "../html.ts"; import { articleHtml } from "../html.ts";
import notFoundHandler from "../handlers/not-found.ts"; import { notFoundHandler } from "../handlers/errors.ts";
import { generateOgProfileImage } from "../magick.ts"; import { generateOgProfileImage } from "../magick.ts";
const userEventHandler = async function (ctx: Context) { const userEventHandler = async function (ctx: Context) {
@ -17,24 +17,20 @@ const userEventHandler = async function (ctx: Context) {
return; return;
} }
try { const articleEvent = await fetchReplaceableEvent(
const articleEvent = await fetchReplaceableEvent( pubkey,
pubkey, identifier,
identifier, );
); const profileEvent = await fetchProfileEvent(pubkey);
const profileEvent = await fetchProfileEvent(pubkey);
if (articleEvent && profileEvent) { if (articleEvent && profileEvent) {
const article = new Article(articleEvent); const article = new Article(articleEvent);
const profile = new Profile(profileEvent, username); const profile = new Profile(profileEvent, username);
const html = await articleHtml(article, profile); const html = await articleHtml(article, profile);
generateOgProfileImage(profile); generateOgProfileImage(profile);
ctx.response.body = html; ctx.response.body = html;
} else { } else {
notFoundHandler(ctx);
}
} catch (_e) {
notFoundHandler(ctx); notFoundHandler(ctx);
} }
}; };

View File

@ -4,7 +4,7 @@ import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
import Article from "../models/article.ts"; import Article from "../models/article.ts";
import Profile from "../models/profile.ts"; import Profile from "../models/profile.ts";
import { profilePageHtml } from "../html.ts"; import { profilePageHtml } from "../html.ts";
import notFoundHandler from "../handlers/not-found.ts"; import { notFoundHandler } from "../handlers/errors.ts";
import { generateOgProfileImage } from "../magick.ts"; import { generateOgProfileImage } from "../magick.ts";
const userProfileHandler = async function (ctx: Context) { const userProfileHandler = async function (ctx: Context) {
@ -16,21 +16,17 @@ const userProfileHandler = async function (ctx: Context) {
return; return;
} }
try { const profileEvent = await fetchProfileEvent(pubkey);
const profileEvent = await fetchProfileEvent(pubkey);
if (profileEvent) { if (profileEvent) {
const profile = new Profile(profileEvent, username); const profile = new Profile(profileEvent, username);
const articleEvents = await fetchArticlesByAuthor(pubkey); const articleEvents = await fetchArticlesByAuthor(pubkey);
const articles = articleEvents.map((a) => new Article(a)); const articles = articleEvents.map((a) => new Article(a));
const html = profilePageHtml(profile, articles); const html = profilePageHtml(profile, articles);
generateOgProfileImage(profile); generateOgProfileImage(profile);
ctx.response.body = html; ctx.response.body = html;
} else { } else {
notFoundHandler(ctx);
}
} catch (_e) {
notFoundHandler(ctx); notFoundHandler(ctx);
} }
}; };

10
ldap.ts
View File

@ -25,10 +25,11 @@ export async function lookupPubkeyByUsername(username: string) {
) { ) {
pubkey = searchEntries[0].nostrKey; pubkey = searchEntries[0].nostrKey;
} }
} catch (e) {
console.error(e);
} finally {
await client.unbind(); await client.unbind();
} catch (e) {
await client.unbind();
throw e;
} }
return pubkey; return pubkey;
@ -49,9 +50,8 @@ export async function lookupUsernameByPubkey(pubkey: string) {
username = searchEntries[0].cn; username = searchEntries[0].cn;
} }
} catch (e) { } catch (e) {
console.error(e);
} finally {
await client.unbind(); await client.unbind();
throw e;
} }
return username; return username;

View File

@ -1,4 +1,4 @@
import { NPool, NRelay1 } from "@nostrify/nostrify"; import { NostrEvent, NostrFilter, NPool, NRelay1 } from "@nostrify/nostrify";
import { nip19 } from "@nostr/tools"; import { nip19 } from "@nostr/tools";
import { lookupUsernameByPubkey } from "./directory.ts"; import { lookupUsernameByPubkey } from "./directory.ts";
import config from "./config.ts"; import config from "./config.ts";
@ -14,47 +14,57 @@ const relayPool = new NPool({
eventRouter: async (_event) => [], eventRouter: async (_event) => [],
}); });
async function fetchWithTimeout(filters: NostrFilter[]) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("relay timeout")), config.query_timeout)
);
const eventsPromise = relayPool.query(filters);
const events = await Promise.race([eventsPromise, timeoutPromise]);
return events;
}
export async function fetchReplaceableEvent( export async function fetchReplaceableEvent(
pubkey: string, pubkey: string,
identifier: string, identifier: string,
) { ) {
let events = await relayPool.query([{ let events = await fetchWithTimeout([{
authors: [pubkey], authors: [pubkey],
kinds: [30023], kinds: [30023],
"#d": [identifier], "#d": [identifier],
limit: 1, limit: 1,
}]); }]) as NostrEvent[];
if (events.length > 0) { if (events.length > 0) {
return events[0]; return events[0];
} else { } else {
events = await relayPool.query([{ events = await fetchWithTimeout([{
authors: [pubkey], authors: [pubkey],
kinds: [30024], kinds: [30024],
"#d": [identifier], "#d": [identifier],
limit: 1, limit: 1,
}]); }]) as NostrEvent[];
return events.length > 0 ? events[0] : null; return events.length > 0 ? events[0] : null;
} }
} }
export async function fetchArticlesByAuthor(pubkey: string) { export async function fetchArticlesByAuthor(pubkey: string) {
const events = await relayPool.query([{ const events = await fetchWithTimeout([{
authors: [pubkey], authors: [pubkey],
kinds: [30023], kinds: [30023],
limit: 10, limit: 10,
}]); }]) as NostrEvent[];
return events; return events;
} }
export async function fetchProfileEvent(pubkey: string) { export async function fetchProfileEvent(pubkey: string) {
const events = await relayPool.query([{ const events = await fetchWithTimeout([{
authors: [pubkey], authors: [pubkey],
kinds: [0], kinds: [0],
limit: 1, limit: 1,
}]); }]) as NostrEvent[];
return events.length > 0 ? events[0] : null; return events.length > 0 ? events[0] : null;
} }

View File

@ -7,23 +7,36 @@ import npubHandler from "./handlers/npub.ts";
import userProfileHandler from "./handlers/user-profile.ts"; import userProfileHandler from "./handlers/user-profile.ts";
import userEventHandler from "./handlers/user-event.ts"; import userEventHandler from "./handlers/user-event.ts";
import userAtomFeedHandler from "./handlers/user-atom-feed.ts"; import userAtomFeedHandler from "./handlers/user-atom-feed.ts";
import notFoundHandler from "./handlers/not-found.ts"; import {
badGatewayHandler,
internalServerErrorHandler,
notFoundHandler,
} from "./handlers/errors.ts";
const router = new Router(); const router = new Router();
router.get("/:path", async (ctx) => { router.get("/:path", async (ctx) => {
const path = ctx.state.path = ctx.params.path; const path = ctx.state.path = ctx.params.path;
if (path.startsWith("naddr")) { try {
await naddrHandler(ctx); if (path.startsWith("naddr")) {
} else if (path.startsWith("nprofile")) { await naddrHandler(ctx);
await nprofileHandler(ctx); } else if (path.startsWith("nprofile")) {
} else if (path.startsWith("npub")) { await nprofileHandler(ctx);
await npubHandler(ctx); } else if (path.startsWith("npub")) {
} else if (path.startsWith("@") || path.startsWith("~")) { await npubHandler(ctx);
await userProfileHandler(ctx); } else if (path.startsWith("@") || path.startsWith("~")) {
} else { await userProfileHandler(ctx);
notFoundHandler(ctx); } else {
notFoundHandler(ctx);
}
} catch (e) {
console.error(e);
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
badGatewayHandler(ctx);
} else {
internalServerErrorHandler(ctx);
}
} }
}); });
@ -36,7 +49,16 @@ router.get("/:username/:kind.atom", async (ctx) => {
(ctx.params.username.startsWith("@") || (ctx.params.username.startsWith("@") ||
ctx.params.username.startsWith("~")) ctx.params.username.startsWith("~"))
) { ) {
await userAtomFeedHandler(ctx); try {
await userAtomFeedHandler(ctx);
} catch (e) {
console.error(e);
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
badGatewayHandler(ctx);
} else {
internalServerErrorHandler(ctx);
}
}
} else { } else {
notFoundHandler(ctx); notFoundHandler(ctx);
} }
@ -47,7 +69,16 @@ router.get("/:username/:identifier", async (ctx) => {
ctx.state.identifier = ctx.params.identifier; ctx.state.identifier = ctx.params.identifier;
if (username.startsWith("@") || username.startsWith("~")) { if (username.startsWith("@") || username.startsWith("~")) {
await userEventHandler(ctx); try {
await userEventHandler(ctx);
} catch (e) {
console.error(e);
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
badGatewayHandler(ctx);
} else {
internalServerErrorHandler(ctx);
}
}
} else { } else {
notFoundHandler(ctx); notFoundHandler(ctx);
} }
@ -66,8 +97,13 @@ router.get("/assets/:path*", async (ctx) => {
} }
await send(ctx, filePath, { root }); await send(ctx, filePath, { root });
} catch (_e) { } catch (e) {
notFoundHandler(ctx); if (e instanceof Error && e.name === "NotFoundError") {
notFoundHandler(ctx);
} else {
console.error(e);
badGatewayHandler(ctx);
}
} }
}); });