Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various enhancements for error handling #44

Merged
merged 1 commit into from
Jan 27, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Amazon.Lambda.APIGatewayEvents
{
using System.Runtime.Serialization;

/// <summary>
/// An object representing the expected format of an API Gateway custom authorizer response.
/// </summary>
[DataContract]
public class APIGatewayCustomAuthorizerContextOutput
{
/// <summary>
/// Gets or sets the 'stringKey' property.
/// </summary>
[DataMember(Name = "stringKey", IsRequired = false)]
public string StringKey { get; set; }

/// <summary>
/// Gets or sets the 'numKey' property.
/// </summary>
[DataMember(Name = "numKey", IsRequired = false)]
public int? NumKey { get; set; }

/// <summary>
/// Gets or sets the 'boolKey' property.
/// </summary>
[DataMember(Name = "boolKey", IsRequired = false)]
public bool? BoolKey { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -24,6 +24,6 @@ public class APIGatewayCustomAuthorizerResponse
/// Gets or sets the <see cref="APIGatewayCustomAuthorizerContext"/> property.
/// </summary>
[DataMember(Name = "context")]
public APIGatewayCustomAuthorizerContext Context { get; set; }
public APIGatewayCustomAuthorizerContextOutput Context { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Text.Encodings.Web;

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.AspNetCoreServer.Internal;

using Amazon.Lambda.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Http.Features;

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace Amazon.Lambda.AspNetCoreServer
{
@@ -71,7 +68,7 @@ protected APIGatewayProxyFunction()
[LambdaSerializerAttribute(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
public virtual async Task<APIGatewayProxyResponse> FunctionHandlerAsync(APIGatewayProxyRequest request, ILambdaContext lambdaContext)
{
lambdaContext?.Logger.Log($"Incoming {request.HttpMethod} requests to {request.Path}");
lambdaContext.Logger.Log($"Incoming {request.HttpMethod} requests to {request.Path}");
InvokeFeatures features = new InvokeFeatures();
MarshallRequest(features, request);
var context = this.CreateContext(features);
@@ -93,41 +90,86 @@ protected HostingApplication.Context CreateContext(IFeatureCollection features)
/// <param name="lambdaContext"><see cref="ILambdaContext"/> implementation.</param>
/// <param name="context">The hosting application request context object.</param>
/// <param name="features">An <see cref="InvokeFeatures"/> instance.</param>
protected async Task<APIGatewayProxyResponse> ProcessRequest(ILambdaContext lambdaContext, HostingApplication.Context context, InvokeFeatures features)
/// <param name="rethrowUnhandledError">
/// If specified, an unhandled exception will be rethrown for custom error handling.
/// Ensure that the error handling code calls 'this.MarshallResponse(features, 500);' after handling the error to return a <see cref="APIGatewayProxyResponse"/> to the user.
/// </param>
protected async Task<APIGatewayProxyResponse> ProcessRequest(ILambdaContext lambdaContext, HostingApplication.Context context, InvokeFeatures features, bool rethrowUnhandledError = false)
{
var defaultStatusCode = 200;
Exception ex = null;
try
{
await this._server.Application.ProcessRequestAsync(context);
this._server.Application.DisposeContext(context, null);
}
catch (AggregateException agex)
{
ex = agex;
lambdaContext.Logger.Log($"Caught AggregateException: '{agex}'");
var sb = new StringBuilder();
foreach (var newEx in agex.InnerExceptions)
{
sb.AppendLine(this.ErrorReport(newEx));
}

lambdaContext.Logger.Log(sb.ToString());
defaultStatusCode = 500;
}
catch (ReflectionTypeLoadException rex)
{
ex = rex;
lambdaContext.Logger.Log($"Caught ReflectionTypeLoadException: '{rex}'");
var sb = new StringBuilder();
foreach (var loaderException in rex.LoaderExceptions)
{
var fileNotFoundException = loaderException as FileNotFoundException;
if (fileNotFoundException != null && !string.IsNullOrEmpty(fileNotFoundException.FileName))
{
sb.AppendLine($"Missing file: {fileNotFoundException.FileName}");
}
else
{
sb.AppendLine(this.ErrorReport(loaderException));
}
}

lambdaContext.Logger.Log(sb.ToString());
defaultStatusCode = 500;
}
catch (Exception e)
{
lambdaContext?.Logger.Log($"Unknown error responding to request: {this.ErrorReport(e)}");
this._server.Application.DisposeContext(context, e);
ex = e;
if (rethrowUnhandledError) throw;
lambdaContext.Logger.Log($"Unknown error responding to request: {this.ErrorReport(e)}");
defaultStatusCode = 500;
}
finally
{
this._server.Application.DisposeContext(context, ex);
}

var response = this.MarshallResponse(features);
var response = this.MarshallResponse(features, defaultStatusCode);

// ASP.NET Core Web API does not always set the status code if the request was
// successful
if (response.StatusCode == 0)
response.StatusCode = defaultStatusCode;
if (ex != null)
response.Headers.Add(new KeyValuePair<string, string>("ErrorType", ex.GetType().Name));

return response;
}

private string ErrorReport(Exception e)
/// <summary>
/// Formats an Exception into a string, including all inner exceptions.
/// </summary>
/// <param name="e"><see cref="Exception"/> instance.</param>
protected string ErrorReport(Exception e)
{
StringBuilder sb = new StringBuilder();
var sb = new StringBuilder();
sb.AppendLine($"{e.GetType().Name}:\n{e}");

Exception inner = e;
while(inner != null)
while (inner != null)
{
Console.WriteLine(inner.Message);
Console.WriteLine(inner.StackTrace);

// Append the messages to the StringBuilder.
sb.AppendLine($"{inner.GetType().Name}:\n{inner}");
inner = inner.InnerException;
}

@@ -201,12 +243,13 @@ protected void MarshallRequest(IHttpRequestFeature requestFeatures, APIGatewayPr
/// serialized into the JSON object that API Gateway expects.
/// </summary>
/// <param name="responseFeatures"></param>
/// <returns></returns>
private APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature responseFeatures)
/// <param name="statusCodeIfNotSet">Sometimes the ASP.NET server doesn't set the status code correctly when successful, so this parameter will be used when the value is 0.</param>
/// <returns><see cref="APIGatewayProxyResponse"/></returns>
protected APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature responseFeatures, int statusCodeIfNotSet = 200)
{
var response = new APIGatewayProxyResponse
{
StatusCode = responseFeatures.StatusCode
StatusCode = responseFeatures.StatusCode != 0 ? responseFeatures.StatusCode : statusCodeIfNotSet
};

if(responseFeatures.Headers != null)
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
using System;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.TestUtilities;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.APIGatewayEvents;
using TestWebApp;
using Xunit;

using Newtonsoft.Json;

namespace Amazon.Lambda.AspNetCoreServer.Test
{
public class TestCallingWebAPI
@@ -21,11 +18,7 @@ public TestCallingWebAPI()
[Fact]
public async Task TestGetAllValues()
{
var lambdaFunction = new LambdaFunction();

var requestStr = File.ReadAllText("values-get-all-apigatway-request.json");
var request = JsonConvert.DeserializeObject<APIGatewayProxyRequest>(requestStr);
var response = await lambdaFunction.FunctionHandlerAsync(request, null);
var response = await this.InvokeAPIGatewayRequest("values-get-all-apigatway-request.json");

Assert.Equal(response.StatusCode, 200);
Assert.Equal("[\"value1\",\"value2\"]", response.Body);
@@ -36,11 +29,7 @@ public async Task TestGetAllValues()
[Fact]
public async Task TestGetSingleValue()
{
var lambdaFunction = new LambdaFunction();

var requestStr = File.ReadAllText("values-get-single-apigatway-request.json");
var request = JsonConvert.DeserializeObject<APIGatewayProxyRequest>(requestStr);
var response = await lambdaFunction.FunctionHandlerAsync(request, null);
var response = await this.InvokeAPIGatewayRequest("values-get-single-apigatway-request.json");

Assert.Equal("value=5", response.Body);
Assert.True(response.Headers.ContainsKey("Content-Type"));
@@ -50,11 +39,7 @@ public async Task TestGetSingleValue()
[Fact]
public async Task TestGetQueryStringValue()
{
var lambdaFunction = new LambdaFunction();

var requestStr = File.ReadAllText("values-get-querystring-apigatway-request.json");
var request = JsonConvert.DeserializeObject<APIGatewayProxyRequest>(requestStr);
var response = await lambdaFunction.FunctionHandlerAsync(request, null);
var response = await this.InvokeAPIGatewayRequest("values-get-querystring-apigatway-request.json");

Assert.Equal("Lewis, Meriwether", response.Body);
Assert.True(response.Headers.ContainsKey("Content-Type"));
@@ -64,11 +49,7 @@ public async Task TestGetQueryStringValue()
[Fact]
public async Task TestPutWithBody()
{
var lambdaFunction = new LambdaFunction();

var requestStr = File.ReadAllText("values-put-withbody-apigatway-request.json");
var request = JsonConvert.DeserializeObject<APIGatewayProxyRequest>(requestStr);
var response = await lambdaFunction.FunctionHandlerAsync(request, null);
var response = await this.InvokeAPIGatewayRequest("values-put-withbody-apigatway-request.json");

Assert.Equal(200, response.StatusCode);
Assert.Equal("Agent, Smith", response.Body);
@@ -79,24 +60,29 @@ public async Task TestPutWithBody()
[Fact]
public async Task TestDefaultResponseErrorCode()
{
var lambdaFunction = new LambdaFunction();
var response = await this.InvokeAPIGatewayRequest("values-get-error-apigatway-request.json");

var requestStr = File.ReadAllText("values-get-error-apigatway-request.json");
var request = JsonConvert.DeserializeObject<APIGatewayProxyRequest>(requestStr);
var response = await lambdaFunction.FunctionHandlerAsync(request, null);
Assert.Equal(response.StatusCode, 500);
Assert.Equal(string.Empty, response.Body);
}

[Theory]
[InlineData("values-get-aggregateerror-apigatway-request.json", "AggregateException")]
[InlineData("values-get-typeloaderror-apigatway-request.json", "ReflectionTypeLoadException")]
public async Task TestEnhancedExceptions(string requestFileName, string expectedExceptionType)
{
var response = await this.InvokeAPIGatewayRequest(requestFileName);

Assert.Equal(response.StatusCode, 500);
Assert.Equal(string.Empty, response.Body);
Assert.True(response.Headers.ContainsKey("ErrorType"));
Assert.Equal(expectedExceptionType, response.Headers["ErrorType"]);
}

[Fact]
public async Task TestGettingSwaggerDefinition()
{
var lambdaFunction = new LambdaFunction();

var requestStr = File.ReadAllText("swagger-get-apigatway-request.json");
var request = JsonConvert.DeserializeObject<APIGatewayProxyRequest>(requestStr);
var response = await lambdaFunction.FunctionHandlerAsync(request, null);
var response = await this.InvokeAPIGatewayRequest("swagger-get-apigatway-request.json");

Assert.Equal(response.StatusCode, 200);
Assert.True(response.Body.Length > 0);
@@ -120,7 +106,7 @@ public void TestCustomAuthorizerSerialization()
var response = new APIGatewayCustomAuthorizerResponse
{
PrincipalID = "com.amazon.someuser",
Context = new APIGatewayCustomAuthorizerContext
Context = new APIGatewayCustomAuthorizerContextOutput
{
StringKey = "Hey I'm a string",
BoolKey = true,
@@ -145,5 +131,14 @@ public void TestCustomAuthorizerSerialization()
var expected = "{\"principalId\":\"com.amazon.someuser\",\"policyDocument\":{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"execute-api:Invoke\"],\"Resource\":[\"arn:aws:execute-api:us-west-2:1234567890:apit123d45/Prod/GET/*\"]}]},\"context\":{\"stringKey\":\"Hey I'm a string\",\"numKey\":9,\"boolKey\":true}}";
Assert.Equal(expected, json);
}

private async Task<APIGatewayProxyResponse> InvokeAPIGatewayRequest(string fileName)
{
var context = new TestLambdaContext();
var lambdaFunction = new LambdaFunction();
var requestStr = File.ReadAllText(fileName);
var request = JsonConvert.DeserializeObject<APIGatewayProxyRequest>(requestStr);
return await lambdaFunction.FunctionHandlerAsync(request, context);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
{
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": false
@@ -9,11 +9,10 @@
"type": "platform",
"version": "1.0.1"
},

"TestWebApp": { "target": "project" },

"xunit": "2.1.0-*",
"dotnet-test-xunit": "2.2.0-*"
"dotnet-test-xunit": "2.2.0-*",
"Amazon.Lambda.TestUtilities": "1.0.0"
},
"testRunner": "xunit",

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"resource": "/{proxy+}",
"path": "/api/errortests",
"httpMethod": "GET",
"headers": null,
"queryStringParameters": {
"id": "aggregate-test"
},
"pathParameters": {
"proxy": "api/values"
},
"stageVariables": null,
"requestContext": {
"accountId": "AAAAAAAAAAAA",
"resourceId": "5agfss",
"stage": "test-invoke-stage",
"requestId": "test-invoke-request",
"identity": {
"cognitoIdentityPoolId": null,
"accountId": "AAAAAAAAAAAA",
"cognitoIdentityId": null,
"caller": "BBBBBBBBBBBB",
"apiKey": "test-invoke-api-key",
"sourceIp": "test-invoke-source-ip",
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": "arn:aws:iam::AAAAAAAAAAAA:root",
"userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)",
"user": "AAAAAAAAAAAA"
},
"resourcePath": "/{proxy+}",
"httpMethod": "GET",
"apiId": "t2yh6sjnmk"
},
"body": null
}
Loading