/* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Amazon.Lambda.RuntimeSupport { // TODO rewrite using a JSON library internal class LambdaJsonExceptionWriter { private static readonly Encoding TEXT_ENCODING = Encoding.UTF8; private const int INDENT_SIZE = 2; private const int MAX_PAYLOAD_SIZE = 256 * 1024; // 256KB private const string ERROR_MESSAGE = "errorMessage"; private const string ERROR_TYPE = "errorType"; private const string STACK_TRACE = "stackTrace"; private const string INNER_EXCEPTION = "cause"; private const string INNER_EXCEPTIONS = "causes"; private const string TRUNCATED_MESSAGE = "{\"" + ERROR_MESSAGE + "\": \"Exception exceeded maximum payload size of 256KB.\"}"; /// /// Write the formatted JSON response for this exception, and all inner exceptions. /// /// The exception response object to serialize. /// The serialized JSON string. public static string WriteJson(ExceptionInfo ex) { if (ex == null) throw new ArgumentNullException("ex"); MeteredStringBuilder jsonBuilder = new MeteredStringBuilder(TEXT_ENCODING, MAX_PAYLOAD_SIZE); string json = AppendJson(ex, 0, false, MAX_PAYLOAD_SIZE - jsonBuilder.SizeInBytes); if (json != null && jsonBuilder.HasRoomForString(json)) { jsonBuilder.Append(json); } else { jsonBuilder.Append(TRUNCATED_MESSAGE); } return jsonBuilder.ToString(); } private static string AppendJson(ExceptionInfo ex, int tab, bool appendComma, int remainingRoom) { if (remainingRoom <= 0) return null; MeteredStringBuilder jsonBuilder = new MeteredStringBuilder(TEXT_ENCODING, remainingRoom); int nextTabDepth = tab + 1; int nextNextTabDepth = nextTabDepth + 1; List jsonElements = new List(); // Grab the elements we want to capture string message = JsonExceptionWriterHelpers.EscapeStringForJson(ex.ErrorMessage); string type = JsonExceptionWriterHelpers.EscapeStringForJson(ex.ErrorType); string stackTrace = ex.StackTrace; ExceptionInfo innerException = ex.InnerException; List innerExceptions = ex.InnerExceptions; // Create the JSON lines for each non-null element string messageJson = null; if (message != null) { // Trim important for Aggregate Exceptions, whose // message contains multiple lines by default messageJson = TabString($"\"{ERROR_MESSAGE}\": \"{message}\"", nextTabDepth); } string typeJson = TabString($"\"{ERROR_TYPE}\": \"{type}\"", nextTabDepth); string stackTraceJson = GetStackTraceJson(stackTrace, nextTabDepth); // Add each non-null element to the json elements list if (typeJson != null) jsonElements.Add(typeJson); if (messageJson != null) jsonElements.Add(messageJson); if (stackTraceJson != null) jsonElements.Add(stackTraceJson); // Exception JSON body, comma delimited string exceptionJsonBody = string.Join("," + Environment.NewLine, jsonElements); jsonBuilder.AppendLine(TabString("{", tab)); jsonBuilder.Append(exceptionJsonBody); bool hasInnerException = innerException != null; bool hasInnerExceptionList = innerExceptions != null && innerExceptions.Count > 0; // Before we close, check for inner exception(s) if (hasInnerException) { // We have to add the inner exception, which means we need // another comma after the exception json body jsonBuilder.AppendLine(","); jsonBuilder.Append(TabString($"\"{INNER_EXCEPTION}\": ", nextTabDepth)); string innerJson = AppendJson(innerException, nextTabDepth, hasInnerExceptionList, remainingRoom - jsonBuilder.SizeInBytes); if (innerJson != null && jsonBuilder.HasRoomForString(innerJson)) { jsonBuilder.Append(innerJson); } else { jsonBuilder.AppendLine(TRUNCATED_MESSAGE); } } if (hasInnerExceptionList) { jsonBuilder.Append(TabString($"\"{INNER_EXCEPTIONS}\": [", nextTabDepth)); for (int i = 0; i < innerExceptions.Count; i++) { var isLastOne = i == innerExceptions.Count - 1; var innerException2 = innerExceptions[i]; string innerJson = AppendJson(innerException2, nextNextTabDepth, !isLastOne, remainingRoom - jsonBuilder.SizeInBytes); if (innerJson != null && jsonBuilder.HasRoomForString(innerJson)) { jsonBuilder.Append(innerJson); } else { jsonBuilder.AppendLine(TabString(TRUNCATED_MESSAGE, nextNextTabDepth)); break; } } jsonBuilder.AppendLine(TabString($"]", nextTabDepth)); } if (!hasInnerException && !hasInnerExceptionList) { // No inner exceptions = no trailing comma needed jsonBuilder.AppendLine(); } jsonBuilder.AppendLine(TabString("}" + (appendComma ? "," : ""), tab)); return jsonBuilder.ToString(); } private static string GetStackTraceJson(string stackTrace, int tab) { if (stackTrace == null) { return null; } string[] stackTraceElements = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None) .Select(s => s.Trim()) .Where(s => !String.IsNullOrWhiteSpace(s)) .Select(s => TabString(($"\"{JsonExceptionWriterHelpers.EscapeStringForJson(s)}\""), tab + 1)) .ToArray(); if (stackTraceElements.Length == 0) { return null; } StringBuilder stackTraceBuilder = new StringBuilder(); stackTraceBuilder.AppendLine(TabString($"\"{STACK_TRACE}\": [", tab)); stackTraceBuilder.AppendLine(string.Join("," + Environment.NewLine, stackTraceElements)); stackTraceBuilder.Append(TabString("]", tab)); return stackTraceBuilder.ToString(); } private static string TabString(string str, int tabDepth) { if (tabDepth == 0) return str; StringBuilder stringBuilder = new StringBuilder(); for (int x = 0; x < tabDepth * INDENT_SIZE; x++) { stringBuilder.Append(" "); } stringBuilder.Append(str); return stringBuilder.ToString(); } } }