I have created an azure function (HTTP triggered with Open API) and deployed to azure. The endpoints working good. I am getting response when testing from Postman
Swagger UI also loading. But when trying to post from Swagger it keeps saying 401 unauthorized. But I have copied the function key from Azure portal (below screen shot) and specified that in authorize popup with in swagger.
But it still saying unauthorized.
When I copy the URL from portal for the http endpoint, it looks like this
https://myurls-asev3.appserviceenvironment.net/api/ObjectRead?code=mycode
Here mycode is exactly the same one I copied from function keys. But the only difference is, the code is attached as a query string in url when I copied the URL from portal
But in swagger it sends as header.
But in function configuration I designed it to accept as header.
[FunctionName("ObjectRead")]
[OpenApiOperation(operationId: "Run", tags: new[] { "name" })]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Header)]
[OpenApiRequestBody("application/json", typeof(FileDetails))]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string), Description = "The OK response")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req)
{
_logger.LogInformation($"ObjectRead function triggered at {DateTime.UtcNow.ToString("O")}.");
var requestBody = await new StreamReader(req.Body).ReadToEndAsync().ConfigureAwait(false);
var data = JsonConvert.DeserializeObject<FileDetails>(requestBody);
var responseMessage = await _objectReadService.ReadAsync(data.FileName, data.FilePath).ConfigureAwait(false);
_logger.LogInformation($"ObjectRead function completed at {DateTime.UtcNow.ToString("O")}.");
return responseMessage.Length != 0 ? new FileStreamResult(responseMessage, "application/octet-stream") { FileDownloadName = data.FileName} : new NotFoundObjectResult("Unable to retrieve the file or File not found.");
}
Above code snippet 3rd line I have mentioned the key as header
Even why its not working in swagger and in url why the code still shows as query string
From the documentation:
https://<APP_NAME>.azurewebsites.net/api/<FUNCTION_NAME>?code=<API_KEY>
The key can be included in a query string variable named code, as above. It can also be included in an x-functions-key HTTP header. The value of the key can be any function key defined for the function, or any host key.
so your attirbute should looks like that:
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "x-functions-key", In = OpenApiSecurityLocationType.Header)]
Related
I created an Azure function which uses a packages that needs a properties file in order to work. From the way they implemented this I need to pass the property file location to the package.
ConfigFactory.setConfigLocation("SomeFolder/AnotherFolder/connector.properties");
When I run this locally this works just fine and everything works as intended. However when I publish it on Azure it tells me the file url is invalid,
[Error] Invalid url to location ]SomeFolder/AnotherFolder/connector.properties[ errorMessage :no protocol: SomeFolder/AnotherFolder/connector.properties file
Am I missing something? How come this doesn't just work on Azure?
Refer to a file in an azure function you have 2 options.
Using Execution Context :
[FunctionName("MyNewHTTPAzureFunction")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log, ExecutionContext context)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name) ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." : $"Hello, {name}. This HTTP triggered function executed successfully.";
//This is how you can access the curent Directory with the help of context.FunctionAppDirectory
string localFile = Path.Combine(context.FunctionAppDirectory, "Data", "Your_file_with_extension");
string readLocalFile = File.ReadAllText(localFile);
...... // your business needs
return new OkObjectResult(responseMessage);
}
Use Path.GetDirectoryName :
To access the directory of the Azure Function if you don’t have access to the ExecutionContext.
You can read the local file in your Startup class like below.
var getPath= Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var rootpath = Path.GetFullPath(Path.Combine(binpath, ".."));
///Read the file
File.ReadAllText(rootpath + "/path/to/filename_with_extension");
Azure Function Access To The Path Is Denied
To avoid the Access Denied error in Azure functions, You Always try to keep your files in the D:\home\site\wwwroot (root Directory) directory that is exactly where exactly the Azure Function resides.
Refer for more information here & Doc for Binding Expression pattern
I have a client using HttpClient.GetAsync to call into a Azure Function Http Trigger in .Net 5.
When I call the function using PostMan, I get my custom header data.
However, when I try to access my response object (HttpResponseMessage) that is returned from HttpClient.GetAsync, my header data empty.
I have my Content data and my Status Code. But my custom header data are missing.
Any insight would be appreciated since I have looking at this for hours.
Thanks for you help.
Edit: Here is the code where I am making the http call:
public async Task<HttpResponseMessage> GetQuotesAsync(int? pageNo, int? pageSize, string searchText)
{
var requestUri = $"{RequestUri.Quotes}?pageNo={pageNo}&pageSize={pageSize}&searchText={searchText}";
return await _httpClient.GetAsync(requestUri);
}
Edit 8/8/2021: See my comment below. The issue has something to do with using Blazor Wasm Client.
For anyone having problems after following the tips on this page, go back and check the configuration in the host.json file. you need the Access-Control-Expose-Headers set to * or they won't be send even if you add them. Note: I added the "extensions" node below and removed my logging settings for clarity.
host.json (sample file):
{
"version": "2.0",
"extensions": {
"http": {
"customHeaders": {
"Access-Control-Expose-Headers": "*"
}
}
}
}
This is because HttpResponseMessage's Headers property data type is HttpResponseHeaders but HttpResponseData's Headers property data type is HttpHeadersCollection. Since, they are different, HttpResponseHeaders could not bind to HttpHeadersCollection while calling HttpClient.GetAsync(as it returns HttpResponseMessage).
I could not find a way to read HttpHeadersCollection through HttpClient.
As long as your Azure function code is emitting the header value, you should be able to read that in your client code from the Headers collection of HttpResponseMessage. Nothing in your azure function (which is your remote endpoint you are calling) makes it any different. Remember, your client code has no idea how your remote endpoint is implemented. Today it is azure functions, tomorrow it may be a full blown aspnet core web api or a REST endpoint written in Node.js. Your client code does not care. All it cares is whether the Http response it received has your expected header.
Asumming you have an azure function like this where you are adding a header called total-count to the response.
[Function("quotes")]
public static async Task<HttpResponseData> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
FunctionContext executionContext)
{
var logger = executionContext.GetLogger("Quotes");
logger.LogInformation("C# HTTP trigger function processed a request for Quotes.");
var quotes = new List<Quote>
{
new Quote { Text = "Hello", ViewCount = 100},
new Quote { Text = "Azure Functions", ViewCount = 200}
};
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("total-count", quotes.Count.ToString());
await response.WriteAsJsonAsync(quotes);
return response;
}
Your existing client code should work as long as you read the Headers property.
public static async Task<HttpResponseMessage> GetQuotesAsync()
{
var requestUri = "https://shkr-playground.azurewebsites.net/api/quotes";
return await _httpClient.GetAsync(requestUri);
}
Now your GetQuotesAsync method can be called somewhere else where you will use the return value of it (HttpResponseMessage instance) and read the headers. In the below example, I am reading that value and adding to a string variable. HttpResponseMessage implements IDisposable. So I am using a using construct to implicitly call the Dispose method.
var msg = "Total count from response headers:";
using (var httpResponseMsg = await GetQuotesAsync())
{
if (httpResponseMsg.Headers.TryGetValues("total-count", out var values))
{
msg += values.FirstOrDefault();
}
}
// TODO: use "msg" variable as needed.
The types which Azure function uses for dealing with response headers is more of an implementation concern of azure functions. It has no impact on your client code where you are using HttpClient and HttpResponseMessage. Your client code is simply dealing with standard http call response (response headers and body)
The issue is not with Blazor WASM, rather if that header has been exposed on your API Side. In your azure function, add the following -
Note: Postman will still show the headers even if you don't expose the headers like below. That's because, Postman doesn't care about CORS headers. CORS is just a browser concept and not a strong security mechanism. It allows you to restrict which other web apps may use your backend resources and that's all.
First create a Startup File to inject the HttpContextAccessor
Package Required: Microsoft.Azure.Functions.Extensions
[assembly: FunctionsStartup(typeof(FuncAppName.Startup))]
namespace FuncAppName
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddScoped<HttpContextAccessor>();
}
}
}
Next, inject it into your main Function -
using Microsoft.AspNetCore.Http;
namespace FuncAppName
{
public class SomeFunction
{
private readonly HttpContext _httpContext;
public SomeFunction(HttpContextAccessor contextAccessor)
{
_httpContext = contextAccessor.HttpContext;
}
[FunctionName("SomeFunc")]
public override Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, new[] { "post" }, Route = "run")] HttpRequest req)
{
var response = "Some Response"
_httpContext.Response.Headers.Add("my-custom-header", "some-custom-value");
_httpContext.Response.Headers.Add("my-other-header", "some-other-value");
_httpContext.Response.Headers.Add("Access-Control-Expose-Headers", "my-custom-header, my-other-header");
return new OkObjectResult(response)
}
If you want to allow all headers you can use wildcard (I think, not tested) -
_httpContext.Response.Headers.Add("Access-Control-Expose-Headers", "*");
You still need to add your web-app url to the azure platform CORS. You can add * wildcard, more info here - https://iotespresso.com/allowing-all-cross-origin-requests-azure-functions/
to enable CORS for Local Apps during development - https://stackoverflow.com/a/60109518/9276081
Now to access those headers in your Blazor WASM, as an e.g. you can -
protected override async Task OnInitializedAsync()
{
var content = JsonContent.Create(new { query = "" });
using (var client = new HttpClient())
{
var result = await client.PostAsync("https://func-app-name.azurewebsites.net/api/run", content);
var headers = result.Headers.ToList();
}
}
TL;DR:
I am calling a WebApi, the WebApi authenticates against the CRM and use the IOrganizationService, so my request is a JObject and not an Entity or EntityReference, it gives me this error:
Error: Type 'Newtonsoft.Json.Linq.JToken' is a recursive collection data contract which is not supported. Consider modifying the definition of collection 'Newtonsoft.Json.Linq.JToken' to remove references to itself.
Context:
I built a web application in angular and I built a WebApi so I can call some custom actions in CRM:
Angular APP | WebApi | OnPremise CRM
So, when I call the WebApi, there is a controller that turns my request into a OrganizationRequest:
Request for WebApi:
{
"ActionName": "custom_actionname",
"Parameters":
[
{
"Key": "EntityInputParameter1",
"Value": {"#odata.type":"Microsoft.Dynamics.CRM.any_entity"}
}
]
}
I read this request on my WebApi and turn that into a request for CRM
Request for CRM:
OrganizationRequest request = new OrganizationRequest("custom_actionname");
request.Parameters["EntityInputParameter1"] = {"#odata.type":"Microsoft.Dynamics.CRM.any_entity"} // This is a JObject
OrganizationResponse response = service.Execute(request);
When I make the request, it gives me the following error:
Error: Type 'Newtonsoft.Json.Linq.JToken' is a recursive collection data contract which is not supported. Consider modifying the definition of collection 'Newtonsoft.Json.Linq.JToken' to remove references to itself.
If I make the request directly to the action it works, but I cannot do that due security policies.
One option could be turn the request into a valid CRM request (parsing {"#odata.type":"Microsoft.Dynamics.CRM.any_entity} into a Entity type) but CRM has a lot of parsing escenarios and could be very complex.
Another option could be sending the request through web and stop using the IOrganizationService but I cannot change that.
I am making this question so anybody that has this error can find the "solution" because I searched a lot and nobody refers this behavior directly.
I am probably turning my InputEntityParameter into string, and I will send the JSON, so I can parse the JSON on my action, but I was looking if anybody else had this error or another approach.
I tested it on one of my Dev Environment with Entity as Parameter.
Below is the code I used in console application to fire Action with Entity as parameter. It ran successfully
var request = new OrganizationRequest("new_test");
//request.Parameters.Add("Target", xAccountReference);
request.Parameters.Add("Param2", "abc");
request.Parameters.Add("Param1", new Entity("account",Guid.Parse("2fe32f22-d01d-ea11-80fa-005056936c69")));
Service.Execute(request);
Below is the Javascript code which used CRM Webapi to execute Action with Parameter. Ignore the XRM.Webapi command but interesting for you would be passing parameters in webapi.
var parameters = {};
parameters.Param2 = "abcd";
var param1 = {};
param1.accountid = "2fe32f22-d01d-ea11-80fa-005056936c69"; //Delete if creating new record
param1["#odata.type"] = "Microsoft.Dynamics.CRM.account";
parameters.Param1 = param1;
var new_testRequest = {
Param2: parameters.Param2,
Param1: parameters.Param1,
getMetadata: function() {
return {
boundParameter: null,
parameterTypes: {
"Param2": {
"typeName": "Edm.String",
"structuralProperty": 1
},
"Param1": {
"typeName": "mscrm.account",
"structuralProperty": 5
}
},
operationType: 0,
operationName: "new_test"
};
}
};
Xrm.WebApi.online.execute(new_testRequest).then(
function success(result) {
if (result.ok) {
//Success - No Return Data - Do Something
}
},
function(error) {
Xrm.Utility.alertDialog(error.message);
}
);
I can confirm that you are mixing Webapi and orgservice call. You can definitely call Action from Webapi of Dynamics. I just used Postman to call Action and I was successful. Blog reference to use Postman for CRM webapi
Below Body as json in Postman and I get Action to run.
{
"Param1":"string test",
"Param2":{
"accountid":"b6b35fd0-b9c3-e311-88e2-00505693000c",
"#odata.type":"Microsoft.Dynamics.CRM.account"
}
}
Edited Thank you #Marco
I'm trying to write a function app that grabs an SVG from a URL and converts it to PNG. I know there are existing API's that do this like CloudConvert, but they don't work nicely with embedded fonts, which is a requirement for me.
Anyway, I wrote a very basic function app that simply downloads a file at this point. Everything works perfectly fine locally, but when I publish to Azure, I get An exception occurred during a WebClient request.
Thanks to #Marco's suggestion, I switched from WebClient to HTTPWebRequest to get more detailed error handling, and as a result, I see the following:
2018-10-11T13:53:53.558 [Info] Function started (Id=e3cbda04-140e-4ef7-ad6c-c871ffe179dd)
2018-10-11T13:53:53.590 [Info] C# HTTP trigger function processed a request.
2018-10-11T13:53:53.752 [Info] Download Fail
2018-10-11T13:53:53.752 [Info] Access to the path
'D:\Windows\system32\734e16961fc276df.svg' is denied.
Am I trying to do something that isn't possible, or is there a fix for this? Is there a way to configure permissions in an Azure function? I need to pull the file down to edit and not just work with the byte array.
Many thanks!
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req,
TraceWriter log, ExecutionContext context)
{
log.Info("C# HTTP trigger function processed a request.");
// parse query parameter
string svgURL = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "l", true) == 0)
.Value;
if (svgURL == null)
{
// Get request body
dynamic data = await req.Content.ReadAsAsync<object>();
svgURL = data?.svgURL;
}
// download file from URL
var uniqueName = GenerateId() ;
try
{
using (var client = new WebClient())
{
client.DownloadFile(svgURL, uniqueName + ".svg" );
}
}
catch (Exception e)
{
log.Info("Download Fail");
log.Info(e.Message);
}
}
The easiest way to solve this is to use temp storage. I can see why Azure wouldn't want functions cludging up the app directory anyway. Updated code below:
I replaced this:
client.DownloadFile(svgURL, uniqueName + ".svg" );
With this:
client.DownloadFile(svgURL, Path.GetTempPath() + "\\" + uniqueName + ".svg" );
Worked like a charm.
Edit:
Below is the GitHub repo where I make this call. There's other stuff going on but you can see where I save to temp storage.
https://github.com/osuhomebase/SVG2PNG-AzureFunction
How can I add a new document to Content Server 10.5 using the REST api?
I am following the Swagger docs for creating a node, but it is not clear how I attach the file to the request. Here is (roughly) the code I am using:
var folderId = 2000;
var docName = "test";
var uri = $"http://[serverName]/otcs/llisapi.dll/api/v1/nodes?type=144&parent_id={folderId}&name={docName}";
var request = new HttpRequestMessage();
request.Headers.Add("Connection", new[] { "Keep-Alive" });
request.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
request.Headers.Add("Pragma", "no-cache");
request.Headers.Add("OTCSTicket", /* ticket here */);
request.RequestUri = new Uri(uri);
request.Method = HttpMethod.Post;
request.Content = new ByteArrayContent(data);
request.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(filePath));
request.Headers.ExpectContinue = false;
var httpClientHandler = new HttpClientHandler
{
Proxy = WebRequest.GetSystemWebProxy(),
UseProxy = true,
AllowAutoRedirect = true
};
using (var client = new HttpClient(httpClientHandler))
{
var response = client.SendAsync(request).Result;
IEnumerable<string> temp;
var vals = response.Headers.TryGetValues("OTCSTicket", out temp) ? temp : new List<string>();
if (vals.Any())
{
this.ticket = vals.First();
}
return response.Content.ReadAsStringAsync().Result;
}
I've been searching through the developer.opentext.com forums, but finding a complete example in c# is proving tough - there are a few examples in javascript, but attempting to replicate these in c# or via chrome or firefox extensions just give the same results. Calling other CS REST methods has not been an issue so far, this is the first one that's giving me problems.
Edit: I pasted the wrong url into my question, which I've now fixed. It was var uri = $"http://[serverName]/otcs/llisapi.dll/api/v1/forms/nodes/create?type=0&parent_id={folderId}&name={docName}";.
Your URL doesn't look like the REST API, it's rather the traditional URL used for the UI.
This article should describe how to do what you want to do:
https://developer.opentext.com/webaccess/#url=%2Fawd%2Fresources%2Farticles%2F6102%2Fcontent%2Bserver%2Brest%2Bapi%2B%2Bquick%2Bstart%2Bguide&tab=501
EDITED:
Ok, so that's how it should work:
send a POST to http://www.your_content_server.com/cs[.exe]/api/v1/nodes
send this in your payload to create a document in your enterprise workspace
type=144
parent_id=2000
name=document_name.txt
<file>
A incomplete demo in Python would look like this. Make sure you get a valid ticket first.
files = {'file': (open("file.txt", 'rb')}
data = { 'type': 144, 'parent_id': 2000, 'name': 'document_name.txt' }
cs = requests.post(url, headers={'otcsticket':'xxxxxxx'}, data=data, files=files)
if cs.status_code == 200:
print "ok"
else:
print cs.text
You will need a form input to get the file onto the page then you can use filestreams to redirect it, there is great guide for that here.
Reading files in JavaScript using the File APIs
Here is a Jquery/ Ajax example.
I find the best way to go about this is to use Postman (Chrome Plugin) to experiment until you get comfortable.
var form = new FormData();
form.append("file", "*filestream*");
form.append("parent_id", "100000");
form.append("name", "NameYourCreatedFile");
form.append("type", "144");
var settings = {
"async": true,
"url": "/cs.exe/api/v1/nodes", // You will need to amend this to match your environment
"method": "POST",
"headers": {
"authorization": "Basic **use Postman to generate this**",
"cache-control": "no-cache",
},
"processData": false,
"contentType": false,
"mimeType": "multipart/form-data",
"data": form
}
$.ajax(settings).done(function (response) {
console.log(response);
});
It appears that the OpenText API only supports file uploads through asynchronous JavaScript uploads - not through traditional file uploads by using typical posted form requests that contain the files contents (which is pretty crappy to be honest - as this would be the easiest to handle on server side).
I've contacted their support and they were absolutely no help - they said since it's working with JavaScript, then they can't help me. Anyone else utilizing any language besides JavaScript is SOL. I submitted my entire API package, but they didn't bother investigating and wanted to close my ticket ASAP.
The only way I've found to do this, is to upload / send the file into your 'Upload' directory on your Content Servers web server (on ours it was set to D:\Upload).This directory location is configurable in the admin section.
Once you've sent the file to your web server, send a create node request with the file param set to the full file path of the file residing on your server, as the OpenText API will attempt to retrieve the file from this directory.
I've created a PHP API for this, and you can browse its usage here:
https://github.com/FBCLIT/OpenTextApi
<?php
use Fbcl\OpenTextApi\Client;
$client = new Client('http://server.com/otcs/cs.exe', 'v1');
$api = $client->connect('username', 'secret');
try {
// The folder node ID of where the file will be created under.
$parentNodeId = '12356';
// The file name to display in OpenText
$fileName = 'My Document.txt';
// The actual file path of the file on the OpenText server.
$serverFilePath = 'D:\Upload\My Document.txt';
$response = $api->createNodeDocument($parentNodeId, $fileName, $serverFilePath);
if (isset($response['id'])) {
// The ID of the newly created document will be returned.
echo $response['id'];
}
} catch (\Exception $ex) {
// File not found on server drive, or issue creating node from given parent.
}
MIME Type detection appears to happen automatically, and you do not need to send anything for it to detect the file type. You can name the file to whatever you like without an extension.
I have also discovered that you cannot use an IP address or Host name for uploading files in this manor. You must enter a path that is local to the server you are uploading to. You can however give just the file name that exists in the Upload directory, and the OpenText API seems to locate it fine.
For example, you can pass either D:\Uploads\Document.txt or Document.txt.
If you haven't done it correctly, you should get the error:
Client error: POST http://server.com/otcs/cs.exe/api/v1/nodes resulted in a 400 Bad Request response: {"error":"Error: File could not be found within the upload directory."}