Skip to content

Commit

Permalink
Merge pull request #44 from thedevopsmachine/master
Browse files Browse the repository at this point in the history
Various enhancements for error handling
  • Loading branch information
normj authored Jan 27, 2017
2 parents 359c128 + e5b40dc commit c744dfb
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 73 deletions.
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
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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)
Expand Down
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
Expand All @@ -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);
Expand All @@ -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"));
Expand All @@ -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"));
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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",

Expand Down
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

0 comments on commit c744dfb

Please sign in to comment.