From 5f5ed0f2c2b24f2c48d5a998d6304137a9e5fc5b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 30 Apr 2026 18:52:16 -0600 Subject: [PATCH] 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 --- apps/dokploy/server/api/routers/compose.ts | 13 ++-- packages/server/src/templates/github.ts | 74 ++++++++++------------ 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index d395bdffc..0398b6ba9 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -700,11 +700,14 @@ export const composeRouter = createTRPCRouter({ getTags: protectedProcedure .input(z.object({ baseUrl: z.string().optional() })) .query(async ({ input }) => { - const githubTemplates = await fetchTemplatesList(input.baseUrl); - - const allTags = githubTemplates.flatMap((template) => template.tags); - const uniqueTags = _.uniq(allTags); - return uniqueTags; + try { + const githubTemplates = await fetchTemplatesList(input.baseUrl); + const allTags = githubTemplates.flatMap((template) => template.tags); + return _.uniq(allTags); + } catch (error) { + console.warn("Failed to fetch template tags:", error); + return []; + } }), disconnectGitProvider: protectedProcedure .input(apiFindCompose) diff --git a/packages/server/src/templates/github.ts b/packages/server/src/templates/github.ts index a935b2155..da697d80c 100644 --- a/packages/server/src/templates/github.ts +++ b/packages/server/src/templates/github.ts @@ -55,25 +55,22 @@ interface TemplateMetadata { export async function fetchTemplatesList( baseUrl = "https://templates.dokploy.com", ): Promise { - try { - const response = await fetch(`${baseUrl}/meta.json`); - 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 response = await fetch(`${baseUrl}/meta.json`, { + signal: AbortSignal.timeout(10000), + }); + 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, + })); } /** @@ -83,27 +80,26 @@ export async function fetchTemplateFiles( templateId: string, baseUrl = "https://templates.dokploy.com", ): Promise<{ config: CompleteTemplate; dockerCompose: string }> { - try { - // Fetch both files in parallel - const [templateYmlResponse, dockerComposeResponse] = await Promise.all([ - fetch(`${baseUrl}/blueprints/${templateId}/template.toml`), - fetch(`${baseUrl}/blueprints/${templateId}/docker-compose.yml`), - ]); + const timeout = AbortSignal.timeout(10000); + const [templateYmlResponse, dockerComposeResponse] = await Promise.all([ + fetch(`${baseUrl}/blueprints/${templateId}/template.toml`, { + signal: timeout, + }), + fetch(`${baseUrl}/blueprints/${templateId}/docker-compose.yml`, { + signal: timeout, + }), + ]); - if (!templateYmlResponse.ok || !dockerComposeResponse.ok) { - 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; + if (!templateYmlResponse.ok || !dockerComposeResponse.ok) { + 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 }; }