{"record":{"id":"b8f637290958950d","repo":"dotnet/yarp","slug":"iproxyconfig-changetoken-has-a-null-value","errorCode":null,"errorMessage":"IProxyConfig.ChangeToken has a null value.","messagePattern":"IProxyConfig\\.ChangeToken has a null value\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/ReverseProxy/Management/ProxyConfigManager.cs","lineNumber":337,"sourceCode":"            Log.ErrorReloadingConfig(_logger, ex);\n\n            foreach (var configChangeListener in _configChangeListeners)\n            {\n                configChangeListener.ConfigurationLoadingFailed(instance.Provider, ex);\n            }\n        }\n    }\n\n    private static void ValidateConfigProperties(IProxyConfig config)\n    {\n        if (config is null)\n        {\n            throw new InvalidOperationException($\"{nameof(IProxyConfigProvider.GetConfig)} returned a null value.\");\n        }\n\n        if (config.ChangeToken is null)\n        {\n            throw new InvalidOperationException($\"{nameof(IProxyConfig.ChangeToken)} has a null value.\");\n        }\n    }\n\n    private ValueTask<IProxyConfig> LoadConfigAsync(IProxyConfigProvider provider, CancellationToken cancellationToken)\n    {\n        var config = provider.GetConfig();\n        ValidateConfigProperties(config);\n\n        if (_destinationResolver.GetType() == typeof(NoOpDestinationResolver))\n        {\n            return new(config);\n        }\n\n        return LoadConfigAsyncCore(config, cancellationToken);\n    }\n\n    private async ValueTask<IProxyConfig> LoadConfigAsyncCore(IProxyConfig config, CancellationToken cancellationToken)\n    {","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/dotnet/yarp/blob/bd11867bee7df522e7fd3effb08a9c85fd616908/src/ReverseProxy/Management/ProxyConfigManager.cs#L319-L355","documentation":"After confirming `GetConfig()` returned non-null, YARP validates that `IProxyConfig.ChangeToken` is not null. The change token is essential because `ProxyConfigManager` uses it to detect config changes and trigger hot reloads. A null change token means YARP can never detect updates, so it refuses to start with this configuration.","triggerScenarios":"A custom `IProxyConfig` implementation (or a custom `IProxyConfigProvider` that constructs one) does not initialize the `ChangeToken` property. During `ValidateConfigProperties` (line 335), the null check fails and throws. The exception propagates through `LoadConfigAsync` and up through the `InitialLoadAsync` wrapper.","commonSituations":"A developer creates a custom `IProxyConfig` class and forgets to set the `ChangeToken` property. A provider manually constructs config objects without using the built-in `ConfigurationConfigProvider` or `InMemoryConfigProvider` which handle this correctly. A config reload path creates a new config object but doesn't carry over the change token.","solutions":["In the custom `IProxyConfig` implementation, always provide a valid `IChangeToken` for `ChangeToken` — use a `CancellationChangeToken` backed by a `CancellationTokenSource` for reload support.","If reloads are not needed, return `NullChangeToken.Singleton` (or a new `CancellationChangeToken(new CancellationTokenSource().Token)`) as a no-op token.","Use the built-in `InMemoryConfigProvider` as a template — it manages a `CancellationTokenSource` and exposes it as the change token.","Add a constructor or factory that always initializes the `ChangeToken` property, preventing null at the source."],"exampleFix":"// before — ChangeToken left null\npublic class MyProxyConfig : IProxyConfig\n{\n    public IReadOnlyList<RouteConfig> Routes { get; set; }\n    public IReadOnlyList<ClusterConfig> Clusters { get; set; }\n    public IChangeToken ChangeToken { get; set; } // null!\n}\n// after — always provide a valid change token\npublic class MyProxyConfig : IProxyConfig\n{\n    private readonly CancellationTokenSource _cts = new();\n    public IReadOnlyList<RouteConfig> Routes { get; init; } = Array.Empty<RouteConfig>();\n    public IReadOnlyList<ClusterConfig> Clusters { get; init; } = Array.Empty<ClusterConfig>();\n    public IChangeToken ChangeToken => new CancellationChangeToken(_cts.Token);\n    internal void TriggerReload() { _cts.Cancel(); }\n}","handlingStrategy":"validation","validationCode":"// Validate ChangeToken before returning config\npublic IProxyConfig GetConfig()\n{\n    var config = BuildConfig();\n    if (config?.ChangeToken is null)\n        throw new InvalidOperationException(\"ProxyConfig must have a non-null ChangeToken\");\n    return config;\n}","typeGuard":"static bool HasValidChangeToken(IProxyConfig? config)\n    => config?.ChangeToken is not null;","tryCatchPattern":"// Not applicable — fix the IProxyConfig implementation to always provide a ChangeToken.","preventionTips":["Always initialize ChangeToken in custom IProxyConfig implementations — use a CancellationChangeToken.","Use NullChangeToken.Singleton if reload notifications are not needed.","Base custom config classes on InMemoryConfigProvider patterns that handle this correctly."],"tags":["configuration","config-provider","null-reference","change-token","contract-violation"],"backgroundTag":null,"analyzedSha":"bd11867bee7df522e7fd3effb08a9c85fd616908","analyzedAt":"2026-08-13T21:29:49.359Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}