diff --git a/apps/dokploy/__test__/dns/ovh.test.ts b/apps/dokploy/__test__/dns/ovh.test.ts index 4f70d4c6c..a58665015 100644 --- a/apps/dokploy/__test__/dns/ovh.test.ts +++ b/apps/dokploy/__test__/dns/ovh.test.ts @@ -440,6 +440,33 @@ describe("ovhClient.updateRecord", () => { /Recreate it manually: A app\.example\.com -> 1\.1\.1\.1/, ); }); + + it("says the change was applied when only the zone refresh fails", async () => { + const cfg = freshConfig(); + mockApi( + ovhSuccess({ + id: 4, + zone: "example.com", + fieldType: "A", + subDomain: "app", + target: "1.1.1.1", + ttl: 60, + }), + ovhSuccess(null), + ovhSuccess({ id: 11 }), + ovhError("Service unavailable", 503), + ); + + // The replacement succeeded, so the record exists at the provider — only + // publishing failed. Rolling back would destroy correct state. + await expect( + ovhClient.updateRecord(cfg, "example.com", "4", { + type: "CNAME", + name: "app.example.com", + content: "example.com", + }), + ).rejects.toThrow(/was applied, but refreshing zone "example\.com" failed/); + }); }); describe("ovhClient.deleteRecord", () => { diff --git a/packages/server/src/utils/dns/ovh.ts b/packages/server/src/utils/dns/ovh.ts index b24666193..73825206d 100644 --- a/packages/server/src/utils/dns/ovh.ts +++ b/packages/server/src/utils/dns/ovh.ts @@ -164,11 +164,23 @@ const mapWithConcurrency = async ( return results; }; -// OVH only applies zone changes once the zone is explicitly refreshed. +// OVH only applies zone changes once the zone is explicitly refreshed. This runs +// after the record write has already succeeded, so a failure here means the +// change exists at the provider but is not being served yet. Rolling the write +// back would destroy correct state over a publish failure, so say what actually +// happened instead of letting the caller read it as "nothing was applied". const refreshZone = async (config: OvhConfig, zone: string) => { - await ovhFetch(config, `/domain/zone/${encodeURIComponent(zone)}/refresh`, { - method: "POST", - }); + try { + await ovhFetch(config, `/domain/zone/${encodeURIComponent(zone)}/refresh`, { + method: "POST", + }); + } catch (error) { + throw new Error( + `OVH: the record change was applied, but refreshing zone "${zone}" failed, so it is not served yet. The next successful change to this zone will publish it, or you can refresh the zone from the OVH manager. Cause: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } }; // Used to undo the delete half of a type change when the replacement fails.