Get route parameters in Azure API Management Inboud policy section - azure

I have a URL with template parameters.
https://test.azure-api.net/HelperFunction/{siteId}/lots/InventoryItem/{itemId}
https://test.azure-api.net/HelperFunction/122/lots/InventoryItem/12
I wanted to read the template/path parameters in inbound policy section.
I am trying as below. But it will only fetch the query string parameters. I wanted to get the path parameters.
<inbound>
<set-body>#{
JObject transBody = new JObject();
transBody.Add("Arguments",
new JObject
{
{"method", context.Request.Method},
{"parameters", context.Request.Url.QueryString},
});
//Add all json properties as arg
transBody.Add("UriPath", context.Request.Url.Path);
return transBody.ToString();
}</set-body>
<base />
</inbound>
My requirement would be to read route parameters as
"siteId" : 122,
"itemId" : 12
Any help would be appreciated.

context.Request.MatchedParameters["siteId"] and context.Request.MatchedParameters["itemId"]
Rest of context variable can be found here: https://learn.microsoft.com/en-us/azure/api-management/api-management-policy-expressions#ContextVariables

Related

Can I force Azure API Management to return 400 instead of 404 when missing required field?

We have an application that requires some fields to be present. If those fields aren't present we will return a 400 response explaining what is missing in a proper error message. Adding APIM to the mix complicates it a lot it seems. Since APIM know that the field is required it looks like it will short curcuit and return 404 with a generic message instead of our self explanatory message of what is wrong.
Is it a way to turn of this functionality for APIM?
I'm getting the same issue and I endup changing my approach. What I did was to configure it on the Application side and use FluentValidation to make the querystring parameters required. So, my model now looks something like this:
using FluentValidation;
public class UrlQueryParameters
{
public string PropertyA { get; set; }
public string PropertyB { get; set; }
}
public class UrlQueryParametersValidator : AbstractValidator<UrlQueryParameters>
{
public UrlQueryParametersValidator()
{
RuleFor(o => o.PropertyA)
.NotEmpty()
.WithMessage("The 'PropertyA' parameter was missing or a value was not provided.");
RuleFor(o => o.PropertyB)
.NotEmpty()
.WithMessage("The 'PropertyB' parameter was missing or a value was not provided.");
}
}
The preceding code defines a couple of validation rules with custom messages for PropertyA and PropertyB properties.
Now, enable FluentValidation as the default validation mechanism for our application by adding the following code at ConfigureServices method of Startup.cs file:
public void ConfigureServices(IServiceCollection services) {
// Rest of the code omitted for brevity
services
.AddControllers()
.AddFluentValidation(fv =>
{
fv.DisableDataAnnotationsValidation = true;
// The following line registers ALL public validators in the assembly containing UrlQueryParametersValidator
// There is no need to register additional validators from this assembly.
fv.RegisterValidatorsFromAssemblyContaining<UrlQueryParametersValidator>(lifetime: ServiceLifetime.Singleton);
});
}
At this point, your API endpoints should validate the required parameters from the request and APIM should not short-circuit the request by throwing 404 Not Found when you try to access /api/foo/{id}.
The reason why this works because Swashbuckle doesn't automatically import validation rules from FluentValidation. Meaning, the properties PropertyA and PropertyB won't be marked as required when viewing them in the Swagger UI. This is the downside for this approach as the required querystring parameters from the Swagger UI will not be marked as required which could be confusing to consumers. But to me, returning the correct StatusCode with meaningful message to consumers is more important and that's why I will stick to this workaround for the time being. You could try using the MicroElements.Swashbuckle.FluentValidation to altleast set/marked the parameters as required in the Swagger UI schema. But that's just about it.
I blogged about this it here: Dirty Hack on Making the Required QueryString Params to Work in Azure APIM
At API/Product/Global level policy add on-error section, use choose policy to check if operation was found or not:
<choose>
<when condition="#(context.LastError.Source == "configuration" && context.LastError.Reason == "OperationNotFound")">
<return-response>
<set-status code="400" reason="Bad Request" />
</return-response>
</when>
</choose>

Response mapping in Azure APIM

Is there any way which can map my response of API into the required format in azure APIM
e.g. if my API gave a response like
customer: {
"id" : 123
}
the output I will be needing is
customer: {
"customerID": 123
}
Is there any way without using a set-body policy?
or is there any way where I can define logic which can map Id value to customerID
There are two options:
Simple JSON
If you have simple JSON as you have shown in the question, then Find and Replace transformation policy can be helpful.
Please note that this policy would just perform string replace. It does not know anything about JSON format. So you have to be extra careful while replacing anything.
But I assume the JSON might not be that simple in real world scenarios.
Complex JSON
You can implement your custom logic by using Set Body Transformation Policy.
Code would be something like below. You can get the JObject and then query and update it.
<choose>
<when condition="#(context.Response.StatusCode == 200">
<set-body>
#{
JObject inBody = context.Request.Body.As<JObject>();
// Razor view syntax so you can write code as per your need
JObject customer= (JObject)inBody["customer"];
var customerIdValue = ((string)customer["id"]);
customer["CustomerId"] = customerId;
customer.Property("id").Remove();
// assuming customer has a description property
customer.Property("description").AddAfterSelf(new JProperty("customerId", customerIdValue));
return inBody.ToString();
}
</set-body>
</when>
</choose>
You could use the Find and Replace String in Body policy for a simple solution.
Could also use set-body with liquid template in outbound of your operation:
<set-body template="liquid">{
"customer": {
"customerID": {{body.customer.id}}
}
}</set-body>

Fetch value from json request body and store in a variable in Azure API Management

I want to fetch a value from json and store it in a variable in Azure API Management.
JSON Example which is coming in request Body is
{
"ItemCode": 1,
"ItemName": "USA",
"typeCode": "REG"
}
I need to fetch value of ItemCode and typeCode and store it in a variable.
I have check on Microsoft Docs and all it gives me to transform body by using liquid template which I suppose is of no use in my requirement.
I have stored a JSON in a variable like
set-variable name="varItemCode" value="#(context.Request.Body.As<String>(preserveContent:true))" />
Since this is stored a string I am not able to traverse the JSON Object.
I was able to do it
<kbd>set-variable name="varTypeCode" value="#{
JObject json = JObject.Parse(context.Variables.GetValueOrDefault<string>("varBody"));
var typeCode = json.GetValue("typeCode");
return typeCode;
}" />
this is working
<set-variable name="validationResults" value="#(context.Request.Body.As<JObject>())" />
<set-variable name="operation" value="#((string)((JObject)context.Variables["validationResults"])["operation"])" />

Azure Api Gateway Error At Set Variable[2]

I am facing this issue for the Azure API Gateway implemented in few of our products :
Message Expression evaluation failed. Object reference not set to an instance of an object.
Exception type GatewayError
Failed method set-variable[2]
{ "statusCode": 500, "message": "Internal server error", "activityId": "bbcf1e2d-c3bd-4020-8a40-cffd99a7f09e" }
This is random. As per my analysis, when I get ResponseCode as 500 or 0 from Back end API, my policy expression evaluation fails where I am capturing RequestMethod, ResponseBody, StatusCode Received, RequestBody, RequestURL and storing the same in a variable.
I read a few troubleshooting articles and have confirmed that the BASE tag is not missing from anywhere under policies. Apparently, this could cause some issues.
Trying to fix the issue, I have now used a conditional operator while setting each variable however the same error still occurs.
<choose>
<when condition="#((string)(context.Variables["RequestOperation"]) == "POST")">
<set-variable name="RequestedBody" value="#(context.Request.Body == null ? " Unable to capture":
context.Request.Body.As<string>(preserveContent: true))" />
</when>
<otherwise>
<set-variable name="RequestedQueryParameters" value="#(context.Request.Url.QueryString == null ||
context.Request.Url.QueryString == "" ? "Unable to capture":(string)context.Request.Url.QueryString)" />
</otherwise>
</choose>
This code is written in the INBOUND policy under the BASE tag.
Policy expression evaluation is failing with the following error message:
Message Expression evaluation failed. Object reference not set to an instance of an object.
Exception type GatewayError
Failed method set-variable[2]
Can someone please help me to solve this problem.
set-variable[2] indicates there is issue in second instance of statement in your policy. I'm unsure whether instance count also . Something you can test very quickly by executing policy with and without and see if instance count changes.
I debug such issues by using try/catch and setting variable or output trace and then use APIMs Test tool to identify issues. Here is what you can try:
`<set-variable name="RequestedQueryParameters" value="#{
try {
return string.IsNullOrEmpty(context.Request.Url.QueryString? "Unable to capture" :(string)context.Request.Url.QueryString);
}
catch (Exception ex)
{
return ex.Message;
}
return string.Empty;
}" />
`
HTH.

Can anyone give me example how to create new IBM Connections Activity using xPages Social Enabler?

Can anyone give me an example how to create new IBM Connections Activity using xPages Social Enabler? I cant find any usefull info in documentation so I have adapted an example from Niklas Heidloff on how to create a new bookmark in Connections. I have the following code for creating a new activity:
try {
var svc = new sbt.ConnectionsService("/activities/service/atom2/activities");
var sb = new java.lang.StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
sb.append("<entry xmlns:snx=\"http://www.ibm.com/xmlns/prod/sn\" xmlns:opensearch=\"http://a9.com/-/spec/opensearch/1.1/\" xmlns:thr=\"http://purl.org/syndication/thread/1.0\" xmlns=\"http://www.w3.org/2005/Atom\">");
sb.append("<title type=\"text\">");
sb.append("test activity from xpages");
sb.append("</title>");
sb.append("<content type=\"html\">");
sb.append("</content>");
sb.append("</entry>");
var msg = svc.post(null, sb.toString(), "xml");
} catch(e) {
print(e)
}
But code above do not create anything but raises error on Domino console. This is returned by svc.post() command:
[31726:00075-3041917840] 11/19/2012 01:03:59 PM HTTP JVM: Client service request to: http://vhost1279.site1.compute.ihost.com:81/activities/service/atom2/activities did not return OK status. Status returned: 415, reason: Unsupported Media Type, expected:information, please consult error-l
[31726:00075-3041917840] 11/19/2012 01:03:59 PM HTTP JVM: g-0.xml located in /local/opt/ibm/lotus/notesdata/domino/workspace/logs
[31726:00075-3041917840] 11/19/2012 01:03:59 PM HTTP JVM: com.ibm.xsp.extlib.sbt.services.client.ClientServicesException: HTTP Status 415, Unsupported Media Type. HTTP error response code received in response to request to url: http://vhost1279.site1.comties/service/atom2/activities
Can anyone give me a hint how to use it properly or point me to some usefull documentation?
Don't use a StringBuilder to create XML. At least use SAX or better Apache Abdera for creating XML (Tutorial here). This ensures that your XML is valid and in case of Abdera also valid ATOM.
It is important to use this approach, since you get a Node object in return, that automatically triggers the needed content type.
Then check how to create an Activity in the Connections documentation wiki (yes - confusing). In this article you find the code to retrieve an activity - I actually recommend to use CURL to get the valid format as an example. Some CURL URLs are here. The closest you get to a full example is Luis' demo of a status update.
To explore connections I use the following batch file:
set server=[server]
set HOME=c:\work
curl %server%%1 –-netrc -G --basic -k -v -L -o %2 %3 %4 %5 %6 %7
with an .netrc file (see the CURL documentation)
machine [server] login [user] password [password]
This is the XML format you need for an activity:
<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
<category scheme="http://www.ibm.com/xmlns/prod/sn/type" term="activity" label="Activity"/>
<title type="text">Posted activity</title>
<content type="html">
This is an activity that has been automatically uploaded from the cURL command line
</content>
</entry>
And post it like this:
post activities/service/atom2/activities newactivity.xml activityresult.xml
Open the activityresult.xml and locate ocate the href attribute of the app:collection element - you need it to add actions. Use the following XML:
<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:snx="http://www.ibm.com/xmlns/prod/sn">
<category scheme="http://www.ibm.com/xmlns/prod/sn/type" term="todo"/>
<category term="Connection4.0"/>
<category term="Test"/>
<title type="text">Some things that need to be done</title>
<content type="html">
This is an <b>action</b> in an activity that has been automatically uploaded from the cURL command line.
</content>
<snx:assignedto>noreply#ibm.com</snx:assignedto>
</entry>
and this command:
post [the-url-you-found-above] newaction.xml actionresult.xml
Once the CURL version works you can try yourself using Abdera code.
Here is a working sample from REST client in Firefox:
https://vhost1279.site1.compute.ihost.com/activities/service/atom2/activities
Header: Content-Type application/atom+xml
<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns:snx="http://www.ibm.com/xmlns/prod/sn" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns="http://www.w3.org/2005/Atom">
<category scheme="http://www.ibm.com/xmlns/prod/sn/type" term="activity" label="Activity" />
<content type="html"/>
<title type="text">
test
</title>
</entry>
The problem with the code above is that you pass in a String to the post method. That however doesn't set the right content type. Please use APIs Stephan suggests to create a org.w3c.dom.Node with the XML and pass this in instead. This will set automatically the right content type in the header.
SOLVED!!! I looked to source and problem was obvious. This is rather a bug or at least misconception but can be easily resolved . According the docs and my testing proves this Connections requires the following header in request: Content-Type = application/atom+xml .... but in source code of Social Enabler I found these two related methods:
protected void prepareRequest(HttpClient httpClient, HttpRequestBase httpRequestBase, Options options) throws ClientServicesException {
// TODO: add support for gzip content
//httpClient.addRequestHeader("Accept-Encoding", "gzip");
if(options.getHeaders()!=null) {
addHeaders(httpClient, httpRequestBase, options);
}
if (options.content != null) {
String contentType = null;
HttpEntity entity = null;
Object content = options.content;
try {
//If a subclass overrides com.ibm.xsp.extlib.services.client.Service.processRequestContent(HttpRequestBase, Object, Options)
//the the subclass must set the content type of the request, and also set the request's entity!
if(processRequestContent(httpClient, httpRequestBase, options)){
if (content instanceof IValue) {
JsonFactory jsFactory = new JsonJavaScriptFactory(DesignerRuntime.getJSContext());
entity = new StringEntity(JsonGenerator.toJson(jsFactory, content, true));
contentType = "application/json";
}
else if (content instanceof JsonObject) {
JsonFactory jsFactory = JsonJavaFactory.instanceEx;
entity = new StringEntity(JsonGenerator.toJson(jsFactory, content, true));
contentType = "application/json";
}
else if (content instanceof Node) {
entity = new StringEntity(DOMUtil.getXMLString((Node) content, true));
contentType = "application/xml";
}
else {
entity = new StringEntity(content.toString());
contentType = findRequestTextContentType(options);
}
}
} catch (Exception ex) {
if(ex instanceof ClientServicesException) {
throw (ClientServicesException)ex;
}
throw new ClientServicesException(ex, "Error while parsing request content");
}
if (entity != null && (httpRequestBase instanceof HttpEntityEnclosingRequestBase)) {
httpRequestBase.setHeader("Content-type", contentType);
((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(entity);
}
}
}
protected String findRequestTextContentType(Options options) {
return "text/plain";
}
As you can see there is no such header (application/atom+xml) for any case. But if you provide XML content as string, the code uses the 'findRequestTextContentType' method to return a default content type, which is 'text/plain' that is not correct for our situation. It is hardcoded so there is no way how to setup the default encoding. But, at least, the 'findRequestTextContentType' is type protected so it can be overriden. So I have created my own ConnectionsService class that extends the former one and overrides the findRequestTextContentType method to return correct content type for my case. And this works fine and resolved the problem !!
import sbt.ConnectionsService;
public class ConnectionsServiceCustom extends ConnectionsService {
public ConnectionsServiceTcl(String serviceUrl) {
super(serviceUrl);
// TODO Auto-generated constructor stub
}
#Override
protected String findRequestTextContentType(Options options) {
return "application/atom+xml";
}
}
As Niklas and Stephen point out you need to use a Dom object (Node, Document etc).. If you are getting an error when creating such an object then it is most likely because the contents of the document/node is poorly formatted or incorrect..
There is a built in XPages util class that allows you to create documents from strings
com.ibm.commons.xml.DOMUtil
Check out
com.ibm.commons.xml.DOMUtil.createDocument(String, String)
e.g.
com.ibm.commons.xml.DOMUtil.createDocument("my xml string", null);
The first parameter is the contents of the XML document, the second is the format.
This class provides several utility methods for parsing and constructing DOM documents.

Resources