I have a simple C# Aws Lambda function which succeeds to a test from the Lambda console test but fails with a 502 (Bad Gateway) if called from the API Gateway (which i generated from the Lambda trigger option) and also if I use postman.(this initial function has open access (no security))
// request header
Content-Type: application/json
// request body
{
"userid":22,
"files":["File1","File2","File3","File4"]
}
The error I get in the logs is:
Wed Feb 08 14:14:54 UTC 2017 : Endpoint response body before transformations: {
"errorType": "NullReferenceException",
"errorMessage": "Object reference not set to an instance of an object.",
"stackTrace": [
"at blahblahmynamespace.Function.FunctionHandler(ZipRequest input, ILambdaContext context)",
"at lambda_method(Closure , Stream , Stream , ContextInfo )"
]
}
It seems like the posted object is not being passed to the lambda input argument.
Code below
// Lambda function
public LambdaResponse FunctionHandler(ZipRequest input, ILambdaContext context)
{
try
{
var logger = context.Logger;
var headers = new Dictionary<string, string>();
if (input == null || input.files.Count == 0)
{
logger.LogLine($"input was null");
headers.Add("testheader", "ohdear");
return new LambdaResponse { body = "fail", headers = headers, statusCode = HttpStatusCode.BadRequest };
}
else
{
logger.LogLine($"recieved request from user{input?.userid}");
logger.LogLine($"recieved {input?.files?.Count} items to zip");
headers.Add("testheader", "yeah");
return new LambdaResponse { body = "hurrah", headers = headers, statusCode = HttpStatusCode.OK };
}
}
catch (Exception ex)
{
throw ex;
}
}
//Lambda response/ZipRequest class
public class LambdaResponse
{
public HttpStatusCode statusCode { get; set; }
public Dictionary<string, string> headers { get; set; }
public string body { get; set; }
}
public class ZipRequest
{
public int userid { get; set; }
public IList<string> files { get; set; }
}
This might not have been available when the OP asked the question, but when invoking a Lambda function using the API Gateway, specific response objects are provided.
As previously noted in the documentation Api Gateway Simple Proxy for Lambda Input Format, the API Gateway wraps the input arguments in a fairly verbose wrapper. It also expects a similarly verbose response object.
However, it is not necessary to create custom request and response objects. The AWS team provides the Amazon.Lambda.APIGatewayEvents library, which is also available on NuGet. This library includes APIGatewayProxyRequest and APIGatewayProxyResponse objects ready-made.
It is still necessary to manually deserialize the Body of the request, as it is a string, not a JSON object. I assume this was done for flexibility?
An example function could look like this. It's a modification of the default function provided by the AWS tools:
public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
{
var bodyString = request?.Body;
if (!string.IsNullOrEmpty(bodyString))
{
dynamic body = JsonConvert.DeserializeObject(bodyString);
if (body.input != null)
{
body.input = body.input?.ToString().ToUpper();
return new APIGatewayProxyResponse
{
StatusCode = 200,
Body = JsonConvert.SerializeObject(body)
};
}
}
return new APIGatewayProxyResponse
{
StatusCode = 200
};
}
When using Lambda Proxy Integration in API Gateway, the first parameter to your FunctionHandler is not the body of your POST, but is another API Gateway-created object, which let's call LambdaRequest. Try these changes to your sample code. Add:
public class LambdaRequest
{
public string body { get; set; }
}
Change your handler prototype to:
public LambdaResponse FunctionHandler(LambdaRequest req, ILambdaContext context)
And inside FunctionHandler add:
ZipRequest input = JsonConvert.DeserializeObject<ZipRequest>(req.Body);
The full LambdaRequest object is documented under Input Format of a Lambda Function for Proxy Integration in the AWS docs, and contains HTTP headers, the HTTP method, the query string, the body, and a few other things.
I also lost a lot of time trying to get a "Path Parameter" passed in on a Get method. For example, if you had a path such as
/appsetting/123
... then you'd have something configured like
By specifying the resource "appid" as {appid} it tells the API Gateway to capture this as a path variable.
One key discovery I found was that by posting in the body of a POST type action, my Lambda would work. Reading around on some other threads, I then discovered I can transform my path variable into a body of the GET action by:
Selecting the GET value (as pictured)
Clicking Integration Request
Creating a mapping template as shown below
Now when I test, I'm able to plug in my appid value in only and get the proper result. Hope this helps someone.
Related
Following the documentation for aws lambda net6:
https://docs.aws.amazon.com/lambda/latest/dg/csharp-handler.html
or small tutorial https://aws.amazon.com/blogs/compute/introducing-the-net-6-runtime-for-aws-lambda/
The simple Top-level lambda code below ignores my simple POST request. The console displays >;0. Why ? Did I forget a package?
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
var handler = async (LambdaInput input, ILambdaContext context) =>
{
Console.Write($">{input.Property1};{input.Property2}");
}
await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer())
.Build()
.RunAsync();
public class LambdaInput
{
public string Property1 { get; set; } = "";
public int Property2 { get; set; }
}
The test.http
POST https://my.api.com/request
Content-Type: application/json
{
"Property1": "Simple text",
"Property2": 8612
}
Question and answer here:
https://github.com/aws/aws-lambda-dotnet/issues/1227
by #normj
You are writing a Lambda function that listens to API Gateway's HTTP events. The event type for those Lambda functions is Amazon.Lambda.APIGatewayEvents.Amazon.Lambda.APIGatewayEvents and should be your first parameter in your function. That event object represents all of the parts of an HTTP request including headers, resource and request body. In your example you can take Body property and serialize into LambdaInput.
You might be interested in checking this new library we are working on that removes some of this complexity into a pattern you might be more use to.
https://aws.amazon.com/blogs/developer/introducing-net-annotations-lambda-framework-preview/
My goal is to call api (via post), accept payload as base type and later cast it to concrete type. If I do that from main solution (where my api stands), everything works well. But I can't understand why same code doesn't work from other solutions.
So I have my request (declared in different solutions)
namespace Nb
{
public class NbRequestBase
{
public string BaseProp { get; set; }
}
public class NbRequestConcrete : NbRequestBase
{
public string ConcreteProp { get; set; }
}
}
And this is my endpoint:
[HttpPost]
[Route("payments/nb")]
public IHttpActionResult Prepare(NbRequestBase request)
{
if(request is NbRequestConcrete)
{
}
try
{
// <<< INSERT CODE HERE >>>
NbRequestConcrete nbRequestConcrete = (NbRequestConcrete)request;
return Ok();
}
catch (Exception ex)
{
_logger.Error(ex);
return InternalServerError();
}
}
and this is my calling code:
NbRequestConcrete requestTwo = new NbRequestConcrete()
{
BaseProp = "BaseProp",
ConcreteProp = "ConcreteProp"
};
using (var client = new HttpClient())
{
var _clientId = "_clientId";
var _clientSecret = "_clientSecret";
client.BaseAddress = new Uri("http://localhost:50228");
#region Formatter
JsonMediaTypeFormatter formatter = new JsonMediaTypeFormatter();
formatter.SerializerSettings.TypeNameHandling = TypeNameHandling.All;
List<MediaTypeFormatter> formatters = new List<MediaTypeFormatter>();
formatters.Add(formatter);
#endregion
var responseMessage = client.PostAsync($"payments/nb?clientId={_clientId}&clientSecret={_clientSecret}", requestTwo, formatter).Result;
responseMessage.EnsureSuccessStatusCode();
}
If I put my calling code into other project/solution (for example just new console app), API endpoint is hit, but payload is null.
payload when called form console app
If I put exacly same calling code into project where my api is (for example in same API endpoint method, at try/catch block start and call it again), API endpoint is hit, payload is NOT null and casting works. Why is it? And how to fix it?
payload when called from same solution try/catch start
And BTW. How to make this call via postman?
Regards
This line tells the model binder to set the values of any matching properties in request to the value that was passed to the API:
public IHttpActionResult Prepare(NbRequestBase request)
The model binder does not attach all the other properties to the request, because it has no idea what they would be.
Problem was Assemblies name where NbRequestConcrete in console app lived in one assembly and on API lived in other. So request was different.
{
"$type": "Nb.NbRequestConcrete, Tester",
"ConcreteProp": "ConcreteProp",
"BaseProp": "BaseProp"
}
VS
{
"$type": "Nb.NbRequestConcrete, MYApi",
"ConcreteProp": "ConcreteProp",
"BaseProp": "BaseProp"
}
I'm executing the URL
https://localhost:44310/api/Licensee/{"name":"stan"}
in address field of my browser, getting the error
"title": "Unsupported Media Type", "status": 415
which is described as
... the origin server is refusing to service the request because the payload
is in a format not supported by this method on the target resource.
The suggested troubleshot is
... due to the request's indicated Content-Type or Content-Encoding, or as a result of inspecting the data ...
I can't really control what header the browser provides. Due to intended usage, I can't rely on Postman or a web application. It needs to be exected from the URL line. The parameter will differ in structure, depending what search criteria that are applied.
The controller looks like this.
[HttpGet("{parameters}")]
public async Task<ActionResult> GetLicensee(LicenseeParameters parameters)
{
return Ok(await Licensee.GetLicenseeByParameters(parameters));
}
I considered decorating the controller with [Consumes("application/json")] but found something dicouraging it. I tried to add JSON converter as suggested here and here but couldn't really work out what option to set, fumbling according to this, not sure if I'm barking up the right tree to begin with.
services.AddControllers()
.AddJsonOptions(_ =>
{
_.JsonSerializerOptions.AllowTrailingCommas = true;
_.JsonSerializerOptions.PropertyNamingPolicy = null;
_.JsonSerializerOptions.DictionaryKeyPolicy = null;
_.JsonSerializerOptions.PropertyNameCaseInsensitive = false;
});
My backup option is to use query string specifying the desired options for a particular search. However, I'd prefer to use the object with parameters for now.
How can I resolve this (or at least troubleshoot further)?
The reason is that there might be a loooot of parameters and I don't want to refactor the controller's signature each time
Actually, you don't have to change the controller's signature each time. ASP.NET Core Model binder is able to bind an object from query string automatically. For example, assume you have a simple controller:
[HttpGet("/api/licensee")]
public IActionResult GetLicensee([FromQuery]LicenseeParameters parameters)
{
return Json(parameters);
}
The first time the DTO is:
public class LicenseeParameters
{
public string Name {get;set;}
public string Note {get;set;}
}
What you need is to send a HTTP Request as below:
GET /api/licensee?name=stan¬e=it+works
And later you decide to change the LicenseeParameters:
public class LicenseeParameters
{
public string Name {get;set;}
public string Note {get;set;}
public List<SubNode> Children{get;set;} // a complex array
}
You don't have to change the controller signature. Just send a payload in this way:
GET /api/licensee?name=stan¬e=it+works&children[0].nodeName=it&children[1].nodeName=minus
The conversion is : . represents property and [] represents collection or dictionary.
In case you do want to send a json string within URL, what you need is to create a custom model binder.
internal class LicenseeParametersModelBinder : IModelBinder
{
private readonly JsonSerializerOptions _jsonOpts;
public LicenseeParametersModelBinder(IOptions<JsonSerializerOptions> jsonOpts)
{
this._jsonOpts = jsonOpts.Value;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var name= bindingContext.FieldName;
var type = bindingContext.ModelType;
try{
var json= bindingContext.ValueProvider.GetValue(name).FirstValue;
var obj = JsonSerializer.Deserialize(json,type, _jsonOpts);
bindingContext.Result = ModelBindingResult.Success(obj);
}
catch (JsonException ex){
bindingContext.ModelState.AddModelError(name,$"{ex.Message}");
}
return Task.CompletedTask;
}
}
and register the model binder as below:
[HttpGet("/api/licensee/{parameters}")]
public IActionResult GetLicensee2([ModelBinder(typeof(LicenseeParametersModelBinder))]LicenseeParameters parameters)
{
return Json(parameters);
}
Finally, you can send a json within URL(suppose the property name is case insensive):
GET /api/licensee/{"name":"stan","note":"it works","children":[{"nodeName":"it"},{"nodeName":"minus"}]}
The above two approaches both works for me. But personally I would suggest you use the the first one as it is a built-in feature.
I have an Azure Function 2.x that reside on a static class that looks like this
[FunctionName("Register")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest req, ILogger log)
{
MyTypeClass defReturn = new MyTypeClass();
HttpStatusCode defCode = HttpStatusCode.BadRequest;
/*
* Logics that might or might not changes
* defReturn and defCode value
*/
return StatusCode((int) defCode, JsonConvert.SerializeObject(defReturn))
}
How can i achieve the return StatusCode((int) defCode, JsonConvert.SerializeObject(defReturn)) part ? is there any such method or equivalent in Azure Functions 2.x ?
in Azure Functions 1.x i can do the equivalent with req.CreateResponse(defCode, defReturn) where req is HttpRequestMessage , but i'm trying to stick with 2.x template/standard
Additional explanation : The said Code should return HTTP 400 Bad Request with the defReturn as it's response body to the client. But when i change the defCode to HttpStatusCode.Accepted, it should return HTTP 202 Accepted with the same response body. How can i achieve this ?
Additional explanation#2 : (If i remember correctly) in ASP.NET Core 1.x i can exactly do like that, returning IActionResult by calling a static method StatusCode not StatusCodes (which is a static class that contains HTTP codes constants
Thank you
Quite late reply, but I was stumbling into the same problem today, so maybe this is helpful for other searchers
Option 1: Default Codes
This is stated in detail on the blog Here
Some codes like 200 and 400 are predefined and can be used by
return new OkObjectResult("Your message"); // 200
return new BadRequestObjectResult("Your error message"); // 400
These functions are not available for every known Status Codes but some of the most frequent.
Option 2: Manual setting Code
If you need specific codes, that are not provided by default, you can use the base classes and create them yourself.
To achieve the Teapot Response for example, you can just use
using Microsoft.AspNetCore.Http;
var result = new ObjectResult("Your message");
result.StatusCode = StatusCodes.Status418ImATeapot;
return result;
In this example, the Statuscode is used from the StatusCodes class, but you can use enter other codes as well (usually, just stick to these codes)
Also, the ObjectResult class offers additional formatting options, if needed.
You can create a model class in which you can define two properties, i.e. one form your status code and one for you Json object and later on you can return the complete model. Code sample would be like below:
public static class QueueTriggerTableOutput
{
[FunctionName("QueueTriggerTableOutput")]
[return: Table("outTable", Connection = "MY_TABLE_STORAGE_ACCT_APP_SETTING")]
public static Person Run(
[QueueTrigger("myqueue-items", Connection = "MY_STORAGE_ACCT_APP_SETTING")]JObject order,
ILogger log)
{
return new Person() {
PartitionKey = "Orders",
RowKey = Guid.NewGuid().ToString(),
Name = order["Name"].ToString(),
MobileNumber = order["MobileNumber"].ToString() };
}
}
public class Person
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public string Name { get; set; }
public string MobileNumber { get; set; }
}
on the receiving front, you can catch both the property.
P.S.- you have to change the return type of your function.
Hope it helps.
I am trying to make a simple api post call using servicestack and it keeps throwing an exception "not found". When the same post call is made directly to the api using a web browser rest api e.g. postman, the api call works.
I have decorated my request object with the route attributes
[Route("/register", "POST")]
public class Register : IReturn<RegistrationResponse>
{
public DateTime? BirthDate { get; set; }
public string Continue { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public string Password { get; set; }
}
The JsonServiceClient is initialised with the base uri but the following call fails
_client = new JsonServiceClient(_apiUri);
_client.HttpMethod = HttpMethods.Post;
var response = _client.Send(body);
The exception that I catch is:
$exception {"Not Found"} System.Exception
{ServiceStack.ServiceClient.Web.WebServiceException} at
ServiceStack.ServiceClient.Web.ServiceClientBase.ThrowWebServiceException[TResponse](Exception
ex, String requestUri) at
ServiceStack.ServiceClient.Web.ServiceClientBase.ThrowResponseTypeException[TResponse](Object
request, Exception ex, String requestUri) at
ServiceStack.ServiceClient.Web.ServiceClientBase.HandleResponseException[TResponse](Exception
ex, Object request, String requestUri, Func1 createWebRequest, Func2
getResponse, TResponse& response) at
ServiceStack.ServiceClient.Web.ServiceClientBase.Send[TResponse](Object
request) at
ApiService`2.Post(String
path, TParams body) in
ApiService.cs:line 81
The documentation on the new API at servicestack mentions the use of the Route attributes decorating the request DTO and the use of the IReturn but from looking at the code behind the Send method, it is working out the rest api url from the name of the request, which implies that your request dto cannot be named anything different.
public virtual TResponse Send<TResponse>(object request)
{
var requestUri = this.SyncReplyBaseUri.WithTrailingSlash() + request.GetType().Name;
var client = SendRequest(requestUri, request);
try
{
var webResponse = client.GetResponse();
return HandleResponse<TResponse>(webResponse);
}
catch (Exception ex)
{
TResponse response;
if (!HandleResponseException(ex,
request,
requestUri,
() => SendRequest(HttpMethods.Post, requestUri, request),
c => c.GetResponse(),
out response))
{
throw;
}
return response;
}
}
What is causing the Not Found exception?
Everthing in your Register class looks correct.
For your client call I would change it to
_client = new JsonServiceClient(_apiUri);
_client.Post(new Register()); //assuming you can map your 'body' variable to a Register class
Just to lose the extra line of code.
it is working out the rest api url from the name of the request, which implies that your request dto cannot be named anything different.
It is working out the endpoint that the rest api will hit. Once it hits the endpoint, the internals of ServiceStack should handle the routing based on the Operation (in this case Register) and Http method. Basically it will try to find a Service class (any class inheriting the Service marker interface) that has the request object (Register) as a parameter and it will use the Http method as the 'function' to call.
What is causing the Not Found exception?
Not exactly sure about this. If you could provide your 'Service' class it may help.
If you have a Service class like
public class RegisterService : Service
{
public RegistrationResponse Post(Register request)
{
//service code
return new RegistrationResponse();
}
}
the routing should work.
The fix for this was to ensure that the servicestack feature for predefined routes was enabled on the api. Once this is done, you don't need to bother with the Route attribute on the request objects.
The end point host config now looks like this:
new EndpointHostConfig
{
DefaultContentType = ContentType.Json,
EnableFeatures = Feature.None
.Add(Feature.Json)
.Add(Feature.PredefinedRoutes),
GlobalResponseHeaders = new Dictionary<string, string>(),
DefaultRedirectPath = "/documentation"
}