Skip to content

Commit b40d3b6

Browse files
RpcException Handling (#11347)
1 parent 9e3c669 commit b40d3b6

File tree

6 files changed

+115
-11
lines changed

6 files changed

+115
-11
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
- Update Node.js Worker Version to [3.12.0](https://github.com/Azure/azure-functions-nodejs-worker/releases/tag/v3.12.0)
2121
- Added support for MCP custom handler. (#11355)
2222
- Update Python Worker Version to [4.40.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.40.0)
23+
- RpcException Handling (#11347)

src/WebJobs.Script.WebHost/Diagnostics/SystemLogger.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -173,9 +173,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
173173
functionName = string.IsNullOrEmpty(fex.MethodName) ? string.Empty : fex.MethodName.Replace("Host.Functions.", string.Empty);
174174
}
175175

176-
(innerExceptionType, innerExceptionMessage, details) = exception.GetExceptionDetails();
177-
formattedMessage = Sanitizer.Sanitize(formattedMessage);
178-
innerExceptionMessage = innerExceptionMessage ?? string.Empty;
176+
(innerExceptionType, innerExceptionMessage, details, formattedMessage) = exception.GetSanitizedExceptionDetails(formattedMessage);
179177
}
180178

181179
_eventGenerator.LogFunctionTraceEvent(logLevel, subscriptionId, appName, functionName, eventName, source, details, formattedMessage, innerExceptionType, innerExceptionMessage, invocationId, _hostInstanceId, activityId, runtimeSiteName, slotName, DateTime.UtcNow);

src/WebJobs.Script.WebHost/Extensions/ExceptionExtensions.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System.Text;
5+
using Microsoft.Azure.WebJobs.Host;
46
using Microsoft.Azure.WebJobs.Logging;
7+
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
8+
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
59

610
namespace System
711
{
812
internal static class ExceptionExtensions
913
{
14+
private const string RedactedMessage = "[Redacted]- Customers using AppInsights or OTel can view full details.";
15+
1016
public static (string ExceptionType, string ExceptionMessage, string ExceptionDetails) GetExceptionDetails(this Exception exception)
1117
{
1218
if (exception == null)
@@ -27,5 +33,58 @@ public static (string ExceptionType, string ExceptionMessage, string ExceptionDe
2733

2834
return (exceptionType, exceptionMessage, exceptionDetails);
2935
}
36+
37+
/// <summary>
38+
/// For FunctionInvocationException with innermost exception as RpcException, the remote message segment is replaced with a redacted
39+
/// placeholder containing a stable hash so that occurrences can still be correlated without exposing the original content.
40+
/// </summary>
41+
/// <param name="exception">
42+
/// The exception instance to inspect. Must not be null.
43+
/// </param>
44+
/// <param name="formattedMessage">
45+
/// A pre-formatted message.
46+
/// </param>
47+
/// <returns>
48+
/// A tuple containing:
49+
/// (InnerExceptionType) The full CLR type name of the base exception.
50+
/// (InnerExceptionMessage) The sanitized and safe base exception message.
51+
/// (Details) The sanitized and safe formatted exception string.
52+
/// (FormattedMessage) The sanitized version of the provided formattedMessage parameter.
53+
/// </returns>
54+
public static (string InnerExceptionType, string InnerExceptionMessage, string Details, string FormattedMessage)
55+
GetSanitizedExceptionDetails(this Exception exception, string formattedMessage)
56+
{
57+
ArgumentNullException.ThrowIfNull(exception);
58+
formattedMessage = Sanitizer.Sanitize(formattedMessage);
59+
60+
var baseException = exception.GetBaseException();
61+
var innerType = baseException.GetType().ToString();
62+
var originalMessage = baseException.Message;
63+
var formattedDetails = exception.ToFormattedString();
64+
65+
if (exception is FunctionInvocationException && baseException is RpcException { RemoteMessage: var remoteMsg }
66+
&& remoteMsg is not null)
67+
{
68+
var redacted = GetRedactedExceptionMessage(remoteMsg);
69+
70+
var innerExceptionMessage = Sanitizer.Sanitize(
71+
originalMessage.Replace(remoteMsg, redacted, StringComparison.Ordinal));
72+
73+
var detailsSanitized = Sanitizer.Sanitize(
74+
formattedDetails.Replace(remoteMsg, redacted, StringComparison.Ordinal));
75+
76+
return (innerType, innerExceptionMessage, detailsSanitized, formattedMessage);
77+
}
78+
79+
var defaultInnerExceptionMessage = Sanitizer.Sanitize(originalMessage);
80+
var defaultDetails = Sanitizer.Sanitize(formattedDetails);
81+
82+
return (innerType, defaultInnerExceptionMessage, defaultDetails, formattedMessage);
83+
}
84+
85+
private static string GetRedactedExceptionMessage(string msg)
86+
{
87+
return $"{RedactedMessage} (Hash: {EncryptionHelper.GetSHA256Base64String(Encoding.UTF8.GetBytes(msg))})";
88+
}
3089
}
3190
}

src/WebJobs.Script.WebHost/Security/EncryptionHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -94,7 +94,7 @@ public static string Decrypt(string value, IEnvironment environment = null)
9494
return Decrypt(key, value);
9595
}
9696

97-
private static string GetSHA256Base64String(byte[] key)
97+
internal static string GetSHA256Base64String(byte[] key)
9898
{
9999
using (var sha256 = SHA256.Create())
100100
{

src/WebJobs.Script/Workers/Rpc/RpcException.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -9,7 +9,7 @@ namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc
99
public class RpcException : Exception
1010
{
1111
public RpcException(string result, string message, string stack, string typeName = "", bool isUserException = false)
12-
: base($"Result: {result}\nException: {Sanitizer.Sanitize(message)}\nStack: {stack}")
12+
: base($"Result: {result}\nType: {typeName}\nException: {Sanitizer.Sanitize(message)}\nStack: {stack}")
1313
{
1414
RemoteStackTrace = stack;
1515
RemoteMessage = Sanitizer.Sanitize(message);

test/WebJobs.Script.Tests/Eventing/SystemLoggerTests.cs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7-
using System.Threading.Tasks;
7+
using System.Text;
88
using Microsoft.Azure.WebJobs.Host;
99
using Microsoft.Azure.WebJobs.Host.Listeners;
1010
using Microsoft.Azure.WebJobs.Logging;
1111
using Microsoft.Azure.WebJobs.Script.Configuration;
1212
using Microsoft.Azure.WebJobs.Script.WebHost;
1313
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
14+
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
15+
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
1416
using Microsoft.Extensions.DependencyInjection;
1517
using Microsoft.Extensions.Hosting;
1618
using Microsoft.Extensions.Logging;
@@ -310,6 +312,50 @@ public void AppEnvironment_Reset_OnSpecialization()
310312
Assert.Equal("updatedruntimesitename", evt.RuntimeSiteName);
311313
}
312314

315+
[Fact]
316+
public void Log_RpcException()
317+
{
318+
string secretString = "{ \"AzureWebJobsStorage\": \"DefaultEndpointsProtocol=https;AccountName=testAccount1;AccountKey=mykey1;EndpointSuffix=core.windows.net\", \"AnotherKey\": \"AnotherValue\" }";
319+
var innerException = new RpcException("result", secretString, "stack", "type");
320+
var functionInvocationException = new FunctionInvocationException("Invocation failed", Guid.Empty, "Functions.TestFunction", innerException);
321+
var formattedMessage = "Test log";
322+
var hash = EncryptionHelper.GetSHA256Base64String(Encoding.UTF8.GetBytes(innerException.RemoteMessage));
323+
var innerExceptionType = innerException.GetType().ToString();
324+
var eventName = string.Empty;
325+
var functionInvocationId = string.Empty;
326+
var activityId = string.Empty;
327+
328+
_mockEventGenerator.Setup(p => p.LogFunctionTraceEvent(LogLevel.Error, _subscriptionId, _websiteName, _functionName, eventName, _category, It.Is<string>(s => s.Contains(hash)),
329+
formattedMessage, innerExceptionType, It.Is<string>(s => s.Contains(hash)), functionInvocationId, _hostInstanceId, activityId, _runtimeSiteName, _slotName, It.IsAny<DateTime>()));
330+
331+
_logger.LogError(functionInvocationException, formattedMessage);
332+
333+
_mockEventGenerator.VerifyAll();
334+
}
335+
336+
[Fact]
337+
public void Log_NonRpcException()
338+
{
339+
var secretReplacement = "[Hidden Credential]";
340+
var secretString = "{ \"AzureWebJobsStorage\": \"DefaultEndpointsProtocol=https;AccountName=testAccount1;AccountKey=mykey1;EndpointSuffix=core.windows.net\", \"AnotherKey\": \"AnotherValue\" }";
341+
var sanitizedString = $"{{ \"AzureWebJobsStorage\": \"{secretReplacement}\", \"AnotherKey\": \"AnotherValue\" }}";
342+
343+
var sanitizedDetails = "System.ArgumentNullException : Value cannot be null. (Parameter 'result')";
344+
var sanitizedExceptionMessage = "Value cannot be null. (Parameter 'result')";
345+
346+
var eventName = string.Empty;
347+
var functionInvocationId = string.Empty;
348+
var activityId = string.Empty;
349+
350+
var ex = new ArgumentNullException("result");
351+
352+
_mockEventGenerator.Setup(p => p.LogFunctionTraceEvent(LogLevel.Error, _subscriptionId, _websiteName, _functionName, eventName, _category, sanitizedDetails, sanitizedString, ex.GetType().ToString(), sanitizedExceptionMessage, functionInvocationId, _hostInstanceId, activityId, _runtimeSiteName, _slotName, It.IsAny<DateTime>()));
353+
354+
_logger.LogError(ex, Sanitizer.Sanitize(secretString));
355+
356+
_mockEventGenerator.VerifyAll();
357+
}
358+
313359
public class FunctionExceptionDataProvider
314360
{
315361
public static IEnumerable<object[]> TestCases

0 commit comments

Comments
 (0)