diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContextOutput.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContextOutput.cs
new file mode 100644
index 000000000..d7dac18ed
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContextOutput.cs
@@ -0,0 +1,29 @@
+namespace Amazon.Lambda.APIGatewayEvents
+{
+ using System.Runtime.Serialization;
+
+ ///
+ /// An object representing the expected format of an API Gateway custom authorizer response.
+ ///
+ [DataContract]
+ public class APIGatewayCustomAuthorizerContextOutput
+ {
+ ///
+ /// Gets or sets the 'stringKey' property.
+ ///
+ [DataMember(Name = "stringKey", IsRequired = false)]
+ public string StringKey { get; set; }
+
+ ///
+ /// Gets or sets the 'numKey' property.
+ ///
+ [DataMember(Name = "numKey", IsRequired = false)]
+ public int? NumKey { get; set; }
+
+ ///
+ /// Gets or sets the 'boolKey' property.
+ ///
+ [DataMember(Name = "boolKey", IsRequired = false)]
+ public bool? BoolKey { get; set; }
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs
index 51218d842..589e5ffd3 100644
--- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs
+++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs
@@ -24,6 +24,6 @@ public class APIGatewayCustomAuthorizerResponse
/// Gets or sets the property.
///
[DataMember(Name = "context")]
- public APIGatewayCustomAuthorizerContext Context { get; set; }
+ public APIGatewayCustomAuthorizerContextOutput Context { get; set; }
}
}
diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs
index b894c25f1..abb50337c 100644
--- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs
+++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs
@@ -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 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)
/// implementation.
/// The hosting application request context object.
/// An instance.
- protected async Task ProcessRequest(ILambdaContext lambdaContext, HostingApplication.Context context, InvokeFeatures features)
+ ///
+ /// 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 to the user.
+ ///
+ protected async Task 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("ErrorType", ex.GetType().Name));
return response;
}
- private string ErrorReport(Exception e)
+ ///
+ /// Formats an Exception into a string, including all inner exceptions.
+ ///
+ /// instance.
+ 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.
///
///
- ///
- private APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature responseFeatures)
+ /// 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.
+ ///
+ 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)
diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs
index 9187fd53b..96c23f0a3 100644
--- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs
+++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs
@@ -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(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(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(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(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(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(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 InvokeAPIGatewayRequest(string fileName)
+ {
+ var context = new TestLambdaContext();
+ var lambdaFunction = new LambdaFunction();
+ var requestStr = File.ReadAllText(fileName);
+ var request = JsonConvert.DeserializeObject(requestStr);
+ return await lambdaFunction.FunctionHandlerAsync(request, context);
+ }
}
}
diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/project.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/project.json
index c36ea99ff..93d4615b0 100644
--- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/project.json
+++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/project.json
@@ -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",
diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-aggregateerror-apigatway-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-aggregateerror-apigatway-request.json
new file mode 100644
index 000000000..811a992ae
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-aggregateerror-apigatway-request.json
@@ -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
+}
\ No newline at end of file
diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-typeloaderror-apigatway-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-typeloaderror-apigatway-request.json
new file mode 100644
index 000000000..0b0c57256
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-typeloaderror-apigatway-request.json
@@ -0,0 +1,36 @@
+{
+ "resource": "/{proxy+}",
+ "path": "/api/errortests",
+ "httpMethod": "GET",
+ "headers": null,
+ "queryStringParameters": {
+ "id": "typeload-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
+}
\ No newline at end of file
diff --git a/Libraries/test/TestWebApp/Controllers/ErrorTestsController.cs b/Libraries/test/TestWebApp/Controllers/ErrorTestsController.cs
index 6a01dde94..e1c1bd993 100644
--- a/Libraries/test/TestWebApp/Controllers/ErrorTestsController.cs
+++ b/Libraries/test/TestWebApp/Controllers/ErrorTestsController.cs
@@ -1,4 +1,6 @@
using System;
+using System.IO;
+using System.Reflection;
using Microsoft.AspNetCore.Mvc;
namespace TestWebApp.Controllers
@@ -9,7 +11,21 @@ public class ErrorTestsController
[HttpGet]
public string Get([FromQuery]string id)
{
- throw new Exception("Unit test exception, for test conditions.");
+ if (id == "typeload-test")
+ {
+ var fnfEx = new FileNotFoundException("Couldn't find file", "System.String.dll");
+ throw new ReflectionTypeLoadException(new[] { typeof(String) }, new[] { fnfEx });
+ }
+
+ var ex = new Exception("Unit test exception, for test conditions.");
+ if (id == "aggregate-test")
+ {
+ throw new AggregateException(ex);
+ }
+ else
+ {
+ throw ex;
+ }
}
}
}