fix(templates): add fetch timeout and handle network errors gracefully

Add 10s AbortSignal timeout to all template fetch calls so they fail
cleanly instead of hanging indefinitely when templates.dokploy.com is
unreachable. Add try/catch to getTags endpoint which was missing error
handling, causing a 500 instead of returning an empty list.

Closes #4282
This commit is contained in:
Mauricio Siu
2026-04-30 18:52:16 -06:00
parent b9ff576682
commit 5f5ed0f2c2
2 changed files with 43 additions and 44 deletions

View File

@@ -700,11 +700,14 @@ export const composeRouter = createTRPCRouter({
getTags: protectedProcedure getTags: protectedProcedure
.input(z.object({ baseUrl: z.string().optional() })) .input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => { .query(async ({ input }) => {
const githubTemplates = await fetchTemplatesList(input.baseUrl); try {
const githubTemplates = await fetchTemplatesList(input.baseUrl);
const allTags = githubTemplates.flatMap((template) => template.tags); const allTags = githubTemplates.flatMap((template) => template.tags);
const uniqueTags = _.uniq(allTags); return _.uniq(allTags);
return uniqueTags; } catch (error) {
console.warn("Failed to fetch template tags:", error);
return [];
}
}), }),
disconnectGitProvider: protectedProcedure disconnectGitProvider: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)

View File

@@ -55,25 +55,22 @@ interface TemplateMetadata {
export async function fetchTemplatesList( export async function fetchTemplatesList(
baseUrl = "https://templates.dokploy.com", baseUrl = "https://templates.dokploy.com",
): Promise<TemplateMetadata[]> { ): Promise<TemplateMetadata[]> {
try { const response = await fetch(`${baseUrl}/meta.json`, {
const response = await fetch(`${baseUrl}/meta.json`); signal: AbortSignal.timeout(10000),
if (!response.ok) { });
throw new Error(`Failed to fetch templates: ${response.statusText}`); if (!response.ok) {
} throw new Error(`Failed to fetch templates: ${response.statusText}`);
const templates = (await response.json()) as TemplateMetadata[];
return templates.map((template) => ({
id: template.id,
name: template.name,
description: template.description,
version: template.version,
logo: template.logo,
links: template.links,
tags: template.tags,
}));
} catch (error) {
console.error("Error fetching templates list:", error);
throw error;
} }
const templates = (await response.json()) as TemplateMetadata[];
return templates.map((template) => ({
id: template.id,
name: template.name,
description: template.description,
version: template.version,
logo: template.logo,
links: template.links,
tags: template.tags,
}));
} }
/** /**
@@ -83,27 +80,26 @@ export async function fetchTemplateFiles(
templateId: string, templateId: string,
baseUrl = "https://templates.dokploy.com", baseUrl = "https://templates.dokploy.com",
): Promise<{ config: CompleteTemplate; dockerCompose: string }> { ): Promise<{ config: CompleteTemplate; dockerCompose: string }> {
try { const timeout = AbortSignal.timeout(10000);
// Fetch both files in parallel const [templateYmlResponse, dockerComposeResponse] = await Promise.all([
const [templateYmlResponse, dockerComposeResponse] = await Promise.all([ fetch(`${baseUrl}/blueprints/${templateId}/template.toml`, {
fetch(`${baseUrl}/blueprints/${templateId}/template.toml`), signal: timeout,
fetch(`${baseUrl}/blueprints/${templateId}/docker-compose.yml`), }),
]); fetch(`${baseUrl}/blueprints/${templateId}/docker-compose.yml`, {
signal: timeout,
}),
]);
if (!templateYmlResponse.ok || !dockerComposeResponse.ok) { if (!templateYmlResponse.ok || !dockerComposeResponse.ok) {
throw new Error("Template files not found"); throw new Error("Template files not found");
}
const [templateYml, dockerCompose] = await Promise.all([
templateYmlResponse.text(),
dockerComposeResponse.text(),
]);
const config = parse(templateYml) as CompleteTemplate;
return { config, dockerCompose };
} catch (error) {
console.error(`Error fetching template ${templateId}:`, error);
throw error;
} }
const [templateYml, dockerCompose] = await Promise.all([
templateYmlResponse.text(),
dockerComposeResponse.text(),
]);
const config = parse(templateYml) as CompleteTemplate;
return { config, dockerCompose };
} }