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; + } } } }