Skip to content

Commit 8e85ebf

Browse files
committed
Max length check/handling for SyncTriggers
1 parent edbfc8e commit 8e85ebf

File tree

3 files changed

+55
-15
lines changed

3 files changed

+55
-15
lines changed

src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool checkHash = fals
9292
return result;
9393
}
9494

95-
var payload = await GetSyncTriggersPayload();
96-
string json = JsonConvert.SerializeObject(payload);
95+
string json = await GetSyncTriggersPayload();
9796

9897
bool shouldSyncTriggers = true;
9998
string newHash = null;
@@ -229,7 +228,7 @@ internal CloudBlockBlob GetHashBlob()
229228
return _hashBlob;
230229
}
231230

232-
public async Task<JToken> GetSyncTriggersPayload()
231+
public async Task<string> GetSyncTriggersPayload()
233232
{
234233
var functionsMetadata = WebFunctionsManager.GetFunctionsMetadata(_hostConfig, _logger);
235234

@@ -240,7 +239,7 @@ public async Task<JToken> GetSyncTriggersPayload()
240239
if (!ArmCacheEnabled)
241240
{
242241
// extended format is disabled - just return triggers
243-
return triggersArray;
242+
return JsonConvert.SerializeObject(triggersArray);
244243
}
245244

246245
// Add triggers to the payload
@@ -293,7 +292,17 @@ public async Task<JToken> GetSyncTriggersPayload()
293292
// like KeyVault when the feature comes online
294293
}
295294

296-
return result;
295+
string json = JsonConvert.SerializeObject(result);
296+
if (json.Length > ScriptConstants.MaxTriggersStringLength)
297+
{
298+
// The settriggers call to the FE enforces a max request size
299+
// limit. If we're over limit, revert to the minimal triggers
300+
// format.
301+
_logger.LogWarning($"SyncTriggers payload of length '{json.Length}' exceeds max length of '{ScriptConstants.MaxTriggersStringLength}'. Reverting to minimal format.");
302+
return JsonConvert.SerializeObject(triggersArray);
303+
}
304+
305+
return json;
297306
}
298307

299308
private async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<FunctionMetadata> functionsMetadata)
@@ -403,9 +412,12 @@ internal static HttpRequestMessage BuildSetTriggersRequest()
403412
if (ArmCacheEnabled)
404413
{
405414
// sanitize the content before logging
406-
var sanitizedContent = JObject.Parse(content);
407-
sanitizedContent.Remove("secrets");
408-
sanitizedContentString = sanitizedContent.ToString();
415+
var sanitizedContent = JToken.Parse(content);
416+
if (sanitizedContent.Type == JTokenType.Object)
417+
{
418+
((JObject)sanitizedContent).Remove("secrets");
419+
sanitizedContentString = sanitizedContent.ToString();
420+
}
409421
}
410422

411423
using (var request = BuildSetTriggersRequest())

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,11 @@ public static class ScriptConstants
100100
public const int MaximumSecretBackupCount = 10;
101101

102102
public const string AzureWebJobsHostsContainerName = "azure-webjobs-hosts";
103+
104+
/// <summary>
105+
/// This constant is also defined in Antares, where the limit is ultimately enforced
106+
/// for settriggers calls. If we raise that limit there, we should raise here as well.
107+
/// </summary>
108+
public const int MaxTriggersStringLength = 102400;
103109
}
104110
}

test/WebJobs.Script.Tests/Management/FunctionsSyncManagerTests.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class FunctionsSyncManagerTests : IDisposable
3636
private readonly MockHttpHandler _mockHttpHandler;
3737
private readonly TestLoggerProvider _loggerProvider;
3838
private readonly Mock<ScriptSettingsManager> _scriptSettingsManagerMock;
39+
private string _function1;
3940

4041
public FunctionsSyncManagerTests()
4142
{
@@ -142,6 +143,27 @@ public async Task TrySyncTriggers_StandbyMode_ReturnsFalse()
142143
}
143144
}
144145

146+
[Fact]
147+
public async Task TrySyncTriggers_MaxSyncTriggersPayloadSize_Succeeds()
148+
{
149+
// create a dummy file that pushes us over size
150+
string maxString = new string('x', ScriptConstants.MaxTriggersStringLength + 1);
151+
_function1 = $"{{ bindings: [], test: '{maxString}'}}";
152+
153+
using (var env = new TestScopedEnvironmentVariable(_vars))
154+
{
155+
Assert.True(_functionsSyncManager.ArmCacheEnabled);
156+
157+
var result = await _functionsSyncManager.TrySyncTriggersAsync();
158+
Assert.True(result.Success);
159+
160+
string syncString = _contentBuilder.ToString();
161+
Assert.True(syncString.Length < ScriptConstants.MaxTriggersStringLength);
162+
var syncContent = JToken.Parse(syncString);
163+
Assert.Equal(JTokenType.Array, syncContent.Type);
164+
}
165+
}
166+
145167
[Fact]
146168
public async Task TrySyncTriggers_LocalEnvironment_ReturnsFalse()
147169
{
@@ -421,7 +443,7 @@ private static HttpClient CreateHttpClient(MockHttpHandler httpHandler)
421443
return new HttpClient(httpHandler);
422444
}
423445

424-
private static IFileSystem CreateFileSystem(ScriptHostConfiguration hostConfig, string hostJsonContent = null)
446+
private IFileSystem CreateFileSystem(ScriptHostConfiguration hostConfig, string hostJsonContent = null)
425447
{
426448
string rootScriptPath = hostConfig.RootScriptPath;
427449
string testDataPath = hostConfig.TestDataPath;
@@ -450,7 +472,7 @@ private static IFileSystem CreateFileSystem(ScriptHostConfiguration hostConfig,
450472
Path.Combine(rootScriptPath, "function3")
451473
});
452474

453-
var function1 = @"{
475+
_function1 = @"{
454476
""scriptFile"": ""main.py"",
455477
""disabled"": false,
456478
""bindings"": [
@@ -497,14 +519,14 @@ private static IFileSystem CreateFileSystem(ScriptHostConfiguration hostConfig,
497519

498520
fileBase.Setup(f => f.Exists(Path.Combine(rootScriptPath, @"function1\function.json"))).Returns(true);
499521
fileBase.Setup(f => f.Exists(Path.Combine(rootScriptPath, @"function1\main.py"))).Returns(true);
500-
fileBase.Setup(f => f.ReadAllText(Path.Combine(rootScriptPath, @"function1\function.json"))).Returns(function1);
522+
fileBase.Setup(f => f.ReadAllText(Path.Combine(rootScriptPath, @"function1\function.json"))).Returns(_function1);
501523
fileBase.Setup(f => f.Open(Path.Combine(rootScriptPath, @"function1\function.json"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
502524
{
503-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
525+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
504526
});
505527
fileBase.Setup(f => f.Open(Path.Combine(testDataPath, "function1.dat"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
506528
{
507-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
529+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
508530
});
509531

510532
fileBase.Setup(f => f.Exists(Path.Combine(rootScriptPath, @"function2\function.json"))).Returns(true);
@@ -516,7 +538,7 @@ private static IFileSystem CreateFileSystem(ScriptHostConfiguration hostConfig,
516538
});
517539
fileBase.Setup(f => f.Open(Path.Combine(testDataPath, "function2.dat"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
518540
{
519-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
541+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
520542
});
521543

522544
fileBase.Setup(f => f.Exists(Path.Combine(rootScriptPath, @"function3\function.json"))).Returns(true);
@@ -528,7 +550,7 @@ private static IFileSystem CreateFileSystem(ScriptHostConfiguration hostConfig,
528550
});
529551
fileBase.Setup(f => f.Open(Path.Combine(testDataPath, "function3.dat"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
530552
{
531-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
553+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
532554
});
533555

534556
return fileSystem.Object;

0 commit comments

Comments
 (0)