fix: remove empty routers/services traefik config instead of writing invalid yaml

Attaching Basic Auth or a Redirect to an application with no domains
wrote an empty routers/services block to the app's dynamic config
file. Traefik's file provider rejects that as invalid and aborts its
watcher, blocking config updates for every other application.

Fixes #5189
This commit is contained in:
Mauricio Siu
2026-08-27 09:37:14 -06:00
parent ca3da4dc5d
commit 032e804114
4 changed files with 134 additions and 13 deletions
@@ -292,6 +292,26 @@ export const writeTraefikConfigRemote = async (
}
};
const isEmptyHttpRoutersAndServices = (traefikConfig: FileConfig) =>
Object.keys(traefikConfig.http?.routers || {}).length === 0 &&
Object.keys(traefikConfig.http?.services || {}).length === 0;
export const writeAppTraefikConfig = async (
traefikConfig: FileConfig,
appName: string,
serverId?: string | null,
) => {
if (isEmptyHttpRoutersAndServices(traefikConfig)) {
await removeTraefikConfig(appName, serverId);
return;
}
if (serverId) {
await writeTraefikConfigRemote(traefikConfig, appName, serverId);
} else {
writeTraefikConfig(traefikConfig, appName);
}
};
export const createServiceConfig = (
appName: string,
domain: Domain,
@@ -3,7 +3,7 @@ import type { ApplicationNested } from "../builders";
import {
loadOrCreateConfig,
loadOrCreateConfigRemote,
writeTraefikConfig,
writeAppTraefikConfig,
writeTraefikConfigRemote,
} from "./application";
import type { FileConfig } from "./file-types";
@@ -89,11 +89,10 @@ export const createRedirectMiddleware = async (
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeMiddleware(config);
writeTraefikConfig(appConfig, appName);
}
await writeAppTraefikConfig(appConfig, appName, serverId);
};
export const removeRedirectMiddleware = async (
@@ -124,9 +123,8 @@ export const removeRedirectMiddleware = async (
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
}
await writeAppTraefikConfig(appConfig, appName, serverId);
};
@@ -4,7 +4,7 @@ import type { ApplicationNested } from "../builders";
import {
loadOrCreateConfig,
loadOrCreateConfigRemote,
writeTraefikConfig,
writeAppTraefikConfig,
writeTraefikConfigRemote,
} from "./application";
import type {
@@ -62,11 +62,10 @@ export const createSecurityMiddleware = async (
addMiddleware(appConfig, middlewareName);
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
}
await writeAppTraefikConfig(appConfig, appName, serverId);
};
export const removeSecurityMiddleware = async (
@@ -89,6 +88,7 @@ export const removeSecurityMiddleware = async (
appConfig = loadOrCreateConfig(appName);
}
const middlewareName = `auth-${appName}`;
let removedLastUser = false;
if (config.http?.middlewares) {
const currentMiddleware = config.http.middlewares[middlewareName];
@@ -106,11 +106,7 @@ export const removeSecurityMiddleware = async (
delete config.http.middlewares[middlewareName];
}
deleteMiddleware(appConfig, middlewareName);
if (serverId) {
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeTraefikConfig(appConfig, appName);
}
removedLastUser = true;
}
}
}
@@ -120,6 +116,9 @@ export const removeSecurityMiddleware = async (
} else {
writeMiddleware(config);
}
if (removedLastUser) {
await writeAppTraefikConfig(appConfig, appName, serverId);
}
};
const isBasicAuthMiddleware = (