Skip to content

Commit

Permalink
Modified APIGatewayProxyFunction to enhance error output for Aggregat…
Browse files Browse the repository at this point in the history
…eExceptions and TypeLoadExceptions.

Refactored TestCallingWebAPI tests to use TestLambdaContext instead of null so that the APIGatewayProxyFunction class can assume ILambdaContext is never null. Also moved commonly called code into a single method.
Created CustomAuthorizerContextOutput class for use by custom authorizers (since the serializer will serialize a null value).
Modified ErrorTestsController to throw different exceptions based on the query string for testing custom error handling.
  • Loading branch information
thedevopsmachine committed Jan 27, 2017
1 parent 359c128 commit e5b40dc
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 e5b40dc

Please sign in to comment.