Azure / azure-functions-host

The host/runtime that powers Azure Functions
https://functions.azure.com
MIT License
1.94k stars 442 forks source link

[CustomHandler]Payload Access in Pure HTTP Azure Function Not Working with Custom Handler #6462

Closed lafernando closed 4 years ago

lafernando commented 4 years ago

I'm using the Ballerina language's custom Azure Functions handler in implementing a pure HTTP function. The code is as follows.

import ballerina/http;
import ballerinax/azure.functions as af;

@af:Function
public function hello(@af:HTTPTrigger { authLevel: "anonymous" } http:Request req) 
                      returns @af:HTTPOutput string|error {
    return "Hello, " + check <@untainted> req.getTextPayload() + "!";
}

I built, deployed, and invoked the function in the following manner:

$ ballerina build functions.bal 
Compiling source
    functions.bal

Generating executables
    functions.jar
    @azure.functions:Function: hello

    Run the following command to deploy Ballerina Azure Functions:
    az functionapp deployment source config-zip -g <resource_group> -n <function_app_name> --src azure-functions.zip

$ az functionapp deployment source config-zip -g functions1777 -n functions1777 --src azure-functions.zip 
Getting scm site credentials for zip deployment
Starting zip deployment. This operation can take a while to complete ...
Deployment endpoint responded with status code 202
{
  "active": false,
  "author": "N/A",
  "author_email": "N/A",
  "complete": true,
  "deployer": "ZipDeploy",
  "end_time": "2020-08-05T03:14:09.1031973Z",
  "id": "83b96c42881b4c61884e3c3731768589",
  "is_readonly": true,
  "is_temp": false,
  "last_success_end_time": "2020-08-05T03:14:09.1031973Z",
  "log_url": "https://functions1777.scm.azurewebsites.net/api/deployments/latest/log",
  "message": "Created via a push deployment",
  "progress": "",
  "provisioningState": null,
  "received_time": "2020-08-05T03:13:58.5080037Z",
  "site_name": "functions1777",
  "start_time": "2020-08-05T03:13:58.9611253Z",
  "status": 4,
  "status_text": "",
  "url": "https://functions1777.scm.azurewebsites.net/api/deployments/latest"
}

$ curl https://functions1777.azurewebsites.net/api/hello -v
...
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: CN=*.azurewebsites.net
*  start date: Sep 24 02:18:56 2019 GMT
*  expire date: Sep 24 02:18:56 2021 GMT
*  subjectAltName: host "functions1777.azurewebsites.net" matched cert's "*.azurewebsites.net"
*  issuer: C=US; ST=Washington; L=Redmond; O=Microsoft Corporation; OU=Microsoft IT; CN=Microsoft IT TLS CA 5
*  SSL certificate verify ok.
> GET /api/hello HTTP/1.1
> Host: functions1777.azurewebsites.net
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
< Set-Cookie: ARRAffinity=bffb580df78d7165f60f8296e74f37e8d5a5ef15cfd49e51bf0d3e3f75c7c66d;Path=/;HttpOnly;Domain=functions1777.azurewebsites.net
< Request-Context: appId=cid-v1:ff099526-44ae-460a-86ca-2d534f6a7258
< Set-Cookie: ARRAffinity=06488d46fdc858266f881d039fdd1144de6081e33ee2d5ada365f466d37d6220;Path=/;HttpOnly;Domain=functions1777.azurewebsites.net
< Date: Wed, 05 Aug 2020 03:19:01 GMT
< Content-Length: 0
< 
* Connection #0 to host functions1777.azurewebsites.net left intact

The logs in Azure Functions App show the following:

2020-08-05T03:24:00.722 [Information] Executing 'Functions.hello' (Reason='This function was programmatically called via the host APIs.', Id=c9287ccd-3d0f-4285-b4ba-3ae4f7ec1287)
2020-08-05T03:24:00.836 [Error] Executed 'Functions.hello' (Failed, Id=c9287ccd-3d0f-4285-b4ba-3ae4f7ec1287, Duration=88ms)
No MediaTypeFormatter is available to read an object of type 'HttpScriptInvocationResult' from content with media type 'text/plain'.

I tried invoking the function with different content types as the input, but I still get the same log. This functionality used to work a little while back (I tested this scenario lastly on July 23'rd 2020), and it's just starting to fail now. Did something change recently in relation to pure HTTP functions? any assistance on this matter is appreciated.

Investigative information

Timestamp: functions1777 Function App version: 2.0 Function App name: functions1777 Function name(s): hello Invocation ID: c9287ccd-3d0f-4285-b4ba-3ae4f7ec1287 Region: East US

anthonychu commented 4 years ago

I think this is related to https://github.com/Azure/azure-functions-host/pull/6308

It should be fixed now, please try again. If it’s not working in core tools, there should be a new release published soon.

Cc @pragnagopa @yojagad

pragnagopa commented 4 years ago

Thanks for reporting. Closing as this should be fixed now. Please reopen if you still run into an issue.

lafernando commented 4 years ago

Hi guys,

I just tested this again, and I'm afraid, I still get the same issue.

The following is the log:-

2020-08-20T22:39:11.151 [Information] Executing 'Functions.hello' (Reason='This function was programmatically called via the host APIs.', Id=b2a5db8a-c41d-48d8-b0c4-01903ec80ca0)
2020-08-20T22:39:11.221 [Error] Executed 'Functions.hello' (Failed, Id=b2a5db8a-c41d-48d8-b0c4-01903ec80ca0, Duration=41ms)
No MediaTypeFormatter is available to read an object of type 'HttpScriptInvocationResult' from content with media type 'text/plain'.

Function App name: functions1777

lafernando commented 4 years ago

@pragnagopa @anthonychu Also, it seems I don't have enough privileges to re-open this issue.

pragnagopa commented 4 years ago

@yojagad - Please investigate.

pragnagopa commented 4 years ago

@lafernando - can you please share host.json contents?

lafernando commented 4 years ago

Hi,

Yes, please find it below.

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "java",
      "defaultWorkerPath": "t.jar",
      "arguments": [
        "-jar"
      ]
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
}
pragnagopa commented 4 years ago

@lafernando - I see you are using the new config section. Forwarding http request is opt-in. Can you please re-try after adding "enableForwardingHttpRequest":true

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "java",
      "defaultWorkerPath": "t.jar",
      "arguments": [
        "-jar"
      ]
    },
"enableForwardingHttpRequest":true
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
}

We are in the process of updating sample and docs, Please see PR https://github.com/Azure/azure-functions-host/issues/6178 for updated samples.

lafernando commented 4 years ago

@pragnagopa - Now I'm getting the following error from my function:

no matching resource found for path : /api/hello , method : POST

This error is coming from the Ballerina custom handler, and it is because, now the custom handler is invoked with the context "/api/hello", earlier it used to be just "/hello". For other non-pure HTTP invocations, I guess the handler will be invoked without the "/api" part?

pragnagopa commented 4 years ago

custom handler is invoked with the context "/api/hello", earlier it used to be just "/hello"

Yes, with the new config we made forwarding simpler. Request will be sent on the same route as original invocation.

For other non-pure HTTP invocations, I guess the handler will be invoked without the "/api" part

That is correct.

Our docs will be updated soon with this info. Thanks!

lafernando commented 4 years ago

custom handler is invoked with the context "/api/hello", earlier it used to be just "/hello"

Yes, with the new config we made forwarding simpler. Request will be sent on the same route as original invocation.

But for example, the "/api" part is mandatory right?, I tried invoking my earlier service with "https://functions1777.azurewebsites.net/hello" and I got a 404. So if "/api" part is anyway going to be there always, wouldn't it make sense to retain the earlier functionality? Also, since now the request come with "/api/" to the handler, now there's a possibility of clashing it with a function that is exposed with the name "api". It seems the situation may have become a bit more complicated :).

For other non-pure HTTP invocations, I guess the handler will be invoked without the "/api" part

That is correct.

Our docs will be updated soon with this info. Thanks!

pragnagopa commented 4 years ago

Please see https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=csharp#customize-the-http-endpoint for information on how http trigger routes work in Azure functions.

Closing this as resolved.

lafernando commented 4 years ago

@pragnagopa I got it, now the custom handler also get the same route as the original request always, and the function name is not involved now. By any chance, is it still possible to get the earlier behavior, maybe without the use of "enableForwardingHttpRequest" flag, where we can still get the HTTP triggered requests invoking the custom handler with the function name? since we can anyway retrieve path parameter information with input bindings, the handler function dispatching would be much simpler/cleaner with the function name in the request.

lafernando commented 4 years ago

@anthonychu Let me put a little bit more context into my request. Unlike a simple custom handler implementation, where the user will implement the handler itself each time writing a function, in a situation like in Ballerina, we automatically generate a handler, which is implemented using a single service, that it will read in the full request path, and by looking at the request path, dispatch it to the functions that are registered.

Earlier, since it used to be that, the path component sent to the handler directly contains the function name, this dispatch was straightforward. But with this new change, we have a situation where for HTTP triggers, we need to do our own mapping of HTTP routes to the function names we have. So when we have multiple routes such as "products/{category:alpha}/{id:int?}" with multiple functions together, the single handler have to resolve different path patterns to a specific function targets. This operation is already done by Azure Functions, and now the handler itself have to implement this logic, which probably can get complicated, with all the patterns supported there, and considering possible priority orders if there are ambiguous situations and so on. I hope my concern is clear here. So it would be great if we can still have a flag with the new configuration, in order for the HTTP trigger based functions to behave in the earlier way as well.

anthonychu commented 4 years ago

Setting enableForwardingHttpRequest to false will invoke the handler at /functionname. The reason we made the change is precisely to remove the need for code like this, where the custom handler needs a special case to deal with a pure HTTP function. Your function dispatcher can now have a single code path no matter what the triggers and bindings are on the function.

As for the case of products/{category:alpha}/{id:int?}, I don't believe the old httpWorker behavior allowed the handler to get the values of the route if it's a pure HTTP function. With customHandler, you can get this information in the payload (with enableForwardingHttpRequest=false) or get it from the HTTP request route (enableForwardingHttpRequest=true).

lafernando commented 4 years ago

Setting enableForwardingHttpRequest to false will invoke the handler at /functionname. The reason we made the change is precisely to remove the need for code like this, where the custom handler needs a special case to deal with a pure HTTP function. Your function dispatcher can now have a single code path no matter what the triggers and bindings are on the function.

@anthonychu That is great! so that means, in the case of a pure HTTP function also (with enableForwardingHttpRequest=false), we would get a JSON payload with a structure like below:

{
    "Data": {
        "httpReq": "{ message: \"Message sent\" }"
    },
    "Metadata": { .. }
}

And yes, with this, we can certainly make the handler generation code much simpler as you pointed out.

Unfortunately, I'm still getting my original issue that I got when I reported this issue (No MediaTypeFormatter is available to read an object of type 'HttpScriptInvocationResult' from content with media type 'text/plain'.). I'm getting this with the following host.json:-

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "java",
      "defaultWorkerPath": "t.jar",
      "arguments": [
        "-jar"
      ]
    },
    "enableForwardingHttpRequest":false
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
}

If we can get this scenario to work, that would be great.

As for the case of products/{category:alpha}/{id:int?}, I don't believe the old httpWorker behavior allowed the handler to get the values of the route if it's a pure HTTP function. With customHandler, you can get this information in the payload (with enableForwardingHttpRequest=false) or get it from the HTTP request route (enableForwardingHttpRequest=true).

Yes, that's my bad, you're correct, earlier with pure HTTP functions we couldn't have input bindings defined, since the payload cannot have that info.

anthonychu commented 4 years ago

@lafernando I just tested out a pure HTTP function with enableForwardingHttpRequest=false and it worked as expected.

With enableForwardingHttpRequest=false, every request to the handler is an HTTP POST. Check that your handler is not getting a 404 on the request.

In one of the future releases of Core Tools, we'll have more trace level logging that will help you diagnose the requests and responses to/from the handler.

lafernando commented 4 years ago

@anthonychu I found the issue! earlier I was sending out the HTTP payload directly from the handler, without wrapping it in the '{"Data":{"req":...}' format (with the enableForwardingHttpRequest=true format). The Azure Functions runtime expected this JSON value, and not seeing that, it has printed the earlier error. The change was simply removing the special casing of pure HTTP function handler generation in my code, and using the general logic.

Thanks a lot, and appreciate taking your time to look into this!

anthonychu commented 4 years ago

No problem. Thanks for all the work on bringing Ballerina to Azure Functions. Please share any feedback you have and let us know if you run into any issues with the new changes. We hope to publish the docs for the new customHandler section soon (when the next version of Core Tools is released).

lafernando commented 4 years ago

Certainly! It's a great system, and very much enjoyed doing the Ballerina implementation. I will surely provide more feedback whenever applicable! Thank you!