question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Using proxy+ API Gateway configuration with 2.0 payload?

See original GitHub issue

I’ve configured a new AWS API Gateway (HTTP) endpoint mapping a route of /{proxy+} to an AWS Lambda .NET Core WebAPI application with multiple endpoints defined internally for WebAPI.

It was failing with a 500 error after setting it up. Cloudwatch logs showed:

Object reference not set to an instance of an object.: NullReferenceException
   at Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction.MarshallRequest(InvokeFeatures features, APIGatewayProxyRequest apiGatewayRequest, ILambdaContext lambdaContext)
   at Amazon.Lambda.AspNetCoreServer.AbstractAspNetCoreFunction`2.FunctionHandlerAsync(TREQUEST request, ILambdaContext lambdaContext)
   at lambda_method(Closure , Stream , Stream , LambdaContextInternal )

I assumed the problem was with in some other part of my configuration (since most of the example code I found for setting this up assumed .NET Core 2.x instead of my 3.x), and so it took me a while to realize that if I go into the Advanced Settings of the route definition, and changed the payload format from the default 2.0 format to the 1.0 format, it would work fine.

Screen Shot 2020-04-23 at 11 51 06 AM

Is there a reason Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction requires the 1.0 payload? Is that optimal/desired?

Also, is there an official AWS documentation of the .NET WebAPI technique using the special /{proxy+} route definition? I actually only realized this setup was possible when I stumbled upon a third party explanation of mapping a single API Gateway route to multiple endpoints in WebAPI. So far have not been able to find official documentation of how it works, etc.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:15 (7 by maintainers)

github_iconTop GitHub Comments

4reactions
normjcommented, Apr 27, 2020

When you want to use HTTP API with 2.0 payload you need to change the base class from APIGatewayProxyFunction to APIGatewayHttpApiV2ProxyFunction. I have a blog post coming out on that covers this very soon.

2reactions
classifieds-devcommented, Apr 30, 2020

I was able to get web sockets working using the standard proxy.

using Microsoft.AspNetCore.Hosting;
using Amazon.Lambda.AspNetCoreServer;
using Microsoft.AspNetCore.Http.Features;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Serilog;
using Amazon.Lambda.AspNetCoreServer.Internal;
using Newtonsoft.Json;
using System.Collections.Generic;
using Chat.Models;
using System.Text;
using System.IO;

namespace Chat
{
    public class Function : APIGatewayProxyFunction
    {

        protected override void Init(IWebHostBuilder builder)
        {
            builder
                .UseSerilog((context, loggerConfiguration) =>
                {
                    loggerConfiguration.ReadFrom.Configuration(context.Configuration);
                }, true, false)
                .UseStartup<Startup>();
        }

        protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxyRequest apiGatewayRequest, ILambdaContext lambdaContext)
        {

            if(apiGatewayRequest.HttpMethod == null)
            {
                apiGatewayRequest.Path = "/";
                apiGatewayRequest.Resource = "/";
                apiGatewayRequest.HttpMethod = "GET";

                lambdaContext.Logger.LogLine("REQUEST: " + JsonConvert.SerializeObject(apiGatewayRequest));
                lambdaContext.Logger.LogLine("CONTEXT: " + JsonConvert.SerializeObject(lambdaContext));
                lambdaContext.Logger.LogLine($"Route Key: {apiGatewayRequest.RequestContext.RouteKey}");

                if (apiGatewayRequest.QueryStringParameters != null && apiGatewayRequest.QueryStringParameters.ContainsKey("token"))
                {
                    var token = apiGatewayRequest.QueryStringParameters["token"];
                    apiGatewayRequest.Headers.Add("Authorization", $"Bearer {token}");
                    apiGatewayRequest.MultiValueHeaders.Add("Authorization", new List<string> { $"Bearer {token}" });
                }

                if (apiGatewayRequest.RequestContext.RouteKey == "messages")
                {
                    var evt = JsonConvert.DeserializeObject<MessagesEvent>(apiGatewayRequest.Body);
                    apiGatewayRequest.QueryStringParameters = new Dictionary<string, string> {
                    { "recipientId", evt.RecipientId }
                };
                    apiGatewayRequest.MultiValueQueryStringParameters = new Dictionary<string, IList<string>> {
                    { "recipientId", new List<string> { evt.RecipientId } }
                };
                }
            }

            lambdaContext.Logger.LogLine("REQUEST: " + JsonConvert.SerializeObject(apiGatewayRequest));
            lambdaContext.Logger.LogLine("CONTEXT: " + JsonConvert.SerializeObject(lambdaContext));

            base.MarshallRequest(features, apiGatewayRequest, lambdaContext);
        }

        protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayProxyRequest apiGatewayRequest, ILambdaContext lambdaContext)
        {
            if(apiGatewayRequest.RequestContext.EventType != null)
            {
                if (apiGatewayRequest.RequestContext.EventType == "CONNECT")
                {
                    aspNetCoreRequestFeature.Path = "/connect";

                }
                else if (apiGatewayRequest.RequestContext.EventType == "DISCONNECT")
                {
                    aspNetCoreRequestFeature.Path = "/disconnect";

                }
                else if (apiGatewayRequest.RequestContext.RouteKey == "conversations")
                {
                    aspNetCoreRequestFeature.Path = "/chatconversations";

                }
                else if (apiGatewayRequest.RequestContext.RouteKey == "message")
                {
                    var evt = JsonConvert.DeserializeObject<MessageEvent>(apiGatewayRequest.Body);
                    var byteArray = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(evt.Message));
                    var stream = new MemoryStream(byteArray);
                    aspNetCoreRequestFeature.Path = "/chatmessage";
                    aspNetCoreRequestFeature.Body = stream;
                    aspNetCoreRequestFeature.Headers.Add("Content-Type", "application/json");
                    aspNetCoreRequestFeature.Headers.Add("Accepts", "application/json");
                    aspNetCoreRequestFeature.Method = "POST";

                }
                else if (apiGatewayRequest.RequestContext.RouteKey == "messages")
                {
                    aspNetCoreRequestFeature.Path = $"/chatmessages";
                }
            }

            if (apiGatewayRequest.RequestContext.EventType == null)
            {
                aspNetCoreRequestFeature.PathBase = "/chat/";
                lambdaContext.Logger.LogLine("CONTEXT: " + JsonConvert.SerializeObject(lambdaContext));

                // The minus one is ensure path is always at least set to `/`
                aspNetCoreRequestFeature.Path =
                    aspNetCoreRequestFeature.Path.Substring(aspNetCoreRequestFeature.PathBase.Length - 1);
                lambdaContext.Logger.LogLine($"Path: {aspNetCoreRequestFeature.Path}, PathBase: {aspNetCoreRequestFeature.PathBase}");
            }

        }

    }
}

The primary trick was settings all this info at the top before calling the parent marshal method.

                apiGatewayRequest.Path = "/";
                apiGatewayRequest.Resource = "/";
                apiGatewayRequest.HttpMethod = "GET";

Once I mocked all that I was successfully able to handle a websocket request like a standard rest api request.

The other piece was than mapping the route keys and event types to their respective rest / http equivalents. That is what is being done PostMarshallRequestFeature.

I’m explicitly mapping all this here. However, I’m sure this could be done in an automated fashion by mapping the body of the request to an event and using the event info to route dynamically based on custom attributes on a class and methods. That would probably be the most ideal solution. However, this seems to work well. It also provides me with the ability to use the lambda in both api gateway and api gateway web socket as a sort of hybrid.

Here is an example of the controller.

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Chat.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Shared.Models;
using Shared.Services;

namespace Chat.Controllers
{
    [ApiController]
    //[Authorize(Policy = "RegisteredUser")]
    public class ChatController : ControllerBase
    {
        private readonly ILogger<ChatController> _logger;
        private readonly ChatRepository _chatRepository;
        private readonly AuthApi _authApi;

        public ChatController(ILogger<ChatController> logger, ChatRepository chatRepository, AuthApi authApi)
        {
            _logger = logger;
            _chatRepository = chatRepository;
            _authApi = authApi;
        }

        [HttpGet("chatconversations")]
        public ActionResult<List<ChatConversation>> Conversations()
        {
            _logger.LogDebug("Get conversations");
            // @todo find user associated with connection.
            // var sub = await _authApi.GetUserId();
            return _chatRepository.GetConversations("123");
        }

        [HttpGet("chatmessages")]
        public ActionResult<List<ChatMessage>> Messages(string recipientId)
        {
            _logger.LogDebug($"Get messages for recipient {recipientId}");
            var userId = "123";
            return _chatRepository.GetMessages(userId, recipientId);
        }

        [HttpPost("chatmessage")]
        public ActionResult<ChatMessage> Message(ChatMessage message)
        {
            _logger.LogDebug("Send message");
            _logger.LogDebug($"message: {JsonConvert.SerializeObject(message)}");
            return message;
        }

        [Route("connect")]
        public ActionResult Connect()
        {
            var request = HttpContext.Items["LambdaRequestObject"] as APIGatewayProxyRequest;
            var context = HttpContext.Items["LambdaContext"] as ILambdaContext;
            // var sub = await _authApi.GetUserId();
            _logger.LogDebug("Connect to web socket");
            _logger.LogDebug(JsonConvert.SerializeObject(request));
            _logger.LogDebug(JsonConvert.SerializeObject(context));
            // _logger.LogDebug($"sub: ${sub}");
            // await _chatRepository.createConnection(request.RequestContext.ConnectionId, "test");
            return StatusCode(200);
        }

        [Route("disconnect")]
        public ActionResult<string> disconnect()
        {
            _logger.LogDebug("Disconnect from websocket");
            return "This is a test";
        }

    }
}

You can basically treat the web socket api the same as a rest api as long as you define the proper mappings. Like I said it would be cool if this where integrated somehow to be dynamic based on attributes. However, as a simple working example what I did seems to function.

This bit here is for authenticating since client side does not allow headers to be set the token must be passed in the query string. However, its easy enough to intercept the request and put the token where the jwt verification process expects it in the hdeaders.

                if (apiGatewayRequest.QueryStringParameters != null && apiGatewayRequest.QueryStringParameters.ContainsKey("token"))
                {
                    var token = apiGatewayRequest.QueryStringParameters["token"];
                    apiGatewayRequest.Headers.Add("Authorization", $"Bearer {token}");
                    apiGatewayRequest.MultiValueHeaders.Add("Authorization", new List<string> { $"Bearer {token}" });
                }

However, that token is only going to be present in the connect. So you need to save the connection and token association somewhere in persistent storage to map the connection id to the user / token in methods / actions called after connecting. I haven’t gotten that far yet. I was going to use cassandra but am having other problems related to that piece.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Working with AWS Lambda proxy integrations for HTTP APIs
Payload format version. The payload format version specifies the format of the data that API Gateway sends to a Lambda integration, and how...
Read more >
API Gateway: Explaining Lambda Payload version 2.0 in ...
Lambda Proxy and its payload. A HTTP request needs to be transformed into a payload which can be sent as an argument to...
Read more >
HTTP API payload format for lambda proxy integration
According to Serverless' documentation the 2.0 payload format is the default format. But when I deploy lambda functions with the Serverless ...
Read more >
Your Complete API Gateway and CORS Guide
Taking full advantage of API Gateway can do a lot to offset the higher ... To enable CORS in a proxy integration, we...
Read more >
Configure Proxies for Your APIs
The proxy enables you to protect your API with the full capabilities of the API gateway, including access to Mule API Analytics. Implements...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found