Custom Hubitat App hitting CORS error

I'm trying to use the feature of slipping HTML into text in apps, along with the very-not-document "mappings" feature to try to see if I can build some more advanced app interfaces. Right now, it is all exploratory. I don't really have an application yet, but I do have a couple I'm tossing around in my head.

The problem I'm running into is this. I can't find any way to get around the CORS issue.

In my app, I have the OAUTH setup, and have verified that my technique works with curl, so the code itself is good. My mappings look like this:

mappings()
{
    path("/test")
    {
        action: [GET: "testGET"]
    }

    path("/testPOST")
    {
        action: [POST: "testPOST"]
    }
}

I've got the functions defined here:

def testGET()
{
    logMessage(type:"info",message:"GET received")
    logMessage(type:"debug",message:"request:  ${request}")
    logMessage(type:"debug",message:"params:  ${params}")

    html="""<html><head><title>GET Received</title></head><body><b>GET Received</b><br/><a href="http://myhubitat.smarthome/installedapp/configure/442/MainPage">Return</a></body></html>"""
    render(contentType: "text/html",data:html,status:200)
}



def testPOST()
{
    logMessage(type:"info",message:"POST received")
    logMessage(type:"debug",message:"request: ${request}")
    logMessage(type:"debug",message:"params: ${params}")

    html="""<html><head><title>POST Received</title></head><body><b>POST Received</b><br/><a href="http://myhubitat.smarthome/installedapp/configure/442/MainPage">Return</a></body></html>"""
    render(contentType: "text/html",data:html,status:200)
}

If I use curl like this:
curl --header "Content-Type: application/json" --request POST --data '{"dummy":"nothing"}' http://192.168.4.56/apps/api/442/testPOST?access_token=<token>

It works just fine. However, when I move over to a web page with code like this in JavaScript:

        function POSTTest()
        {
            params=new URLSearchParams({
                access_token: "<token>"
            })

            headers=new Headers();

            headers.append('Content-Type','applicatoin/json')
            headers.append('method','no-cors')

            fetch("http://192.168.4.56/apps/api/442/testPOST?" + params.toString(),
                {
                    method: "POST",
                    body: JSON.stringify({dummy: "nothing"}),
                    headers: headers
                }
            )
            .then((response)=>{
                if(!response.ok){
                    console.log("Network response was not ok")
                }
                return response.json()
            })
            .then(data=>{
                console.log(data)
            })
            .catch(error=>{
                console.log(error)
            })

        }

It fails. Taking out the no-cors doesn't help at all. With it or without it, I get the Fetch Failed error related to a CORS issue.

TypeError: Failed to fetch
at POSTTest (c:\Users\kurtg\Documents\Hubitat Code Archive\SVG Experiments\test1.html:212:13)
at HTMLButtonElement.onclick (file:////test1.html:18:34) {stack: 'TypeError: Failed to fetch
at POSTTest (f…20Archive/SVG%20Experiments/test1.html:18:34)', message: 'Failed to fetch'}

I did see that MAKER API has an option to get around the CORS issue, but I can't POST to MAKER. I anticipate cases where I want to move a fairly large amount of data and doing that via the query string really seems stupid and maybe not even possible.

Is there a way around this? If not, what good is the mappings function? I get the CORS error on GET requests too, but because of the way a GET is processed verses a POST, it "sneaks through" and works.

Some of the things I've tried are embedding my JavaScript right into the App definition (which is my final goal, but for now I'm testing with stand alone web pages) and I've tried running off computers on the same VLAN with the HE. I always get the same error. The only thing that does work is curl.

Thoughts?

Check out my remote builder code, specifically SmartGrid which is shown below.

Bi-directional communication between a JS app and the Hub with the app being embedded in the Groovy app and being delivered over an endpoint.

You can play with a live version here using virtual devices on my old C-5..

I don't use CURL though. Did not have any need to.

3 Likes

Thanks for the reply. I've used techniques like this before, and they can and do work just fine for some really cool things (like your remote idea...pretty interesting). What I'm struggling with is how to do this with "a lot of data". If I only have a few pieces of information to transfer, a simple query string on a URL will work, and I can push that through a driver, via Maker API just fine. But what happens if I want to push a lot of data through? I'm not talking about pushing pictures or entire books through, but what about wanting to update say 5 parameters on 20 devices all at once (completely hypothetical, but it makes my point). Then it starts to fail. Any communication through a driver requires either many, multiple calls, or a lot of data stuck in one custom variable (I've done that...not great...but it works).

That is why I'm intrigued by this idea of custom URL endpoints via the "mappings" in the app. This would allow me to use a POST message to move a JSON cleanly and efficiently, with "lots of data" in it. However, due to the CORS problem, it won't work.

To be honest, I don't understand CORS. It seems like an almost useless feature that can only cause problems, but I guess maybe I don't understand how it really gets implements. What I really don't get is why CURL doesn't care about it, but Chrome does. I don't understand how that makes any sense.

This was my JSON upload to the hub from changing 1 parameter on 5 devices simultaneously.

Uploading device data via toHub(): [{"dID":"2824","switch":"off"},{"dID":"2822","switch":"off"},{"dID":"2825","switch":"off"},{"dID":"2823","switch":"off"},{"dID":"2814","switch":"off"}]

So changing 5 parameters on 20 devices would be very manageable using JSON IMO if that is truly the upper edge use case.

When I first started writing this I also had a similar issue when I was writing the app in a text-editor using the appropriate endpoint address. Then I found this header which would allow me to send data, but all I could get back was a success\fail type of response. So I could send commands and they would work but I could not get back a proper status.

fetch('https://example.com/some-resource', {
    method: 'GET',
    mode: 'no-cors'
})

But, once I delivered the JS in the HUB response then the query was no longer cross-origin and I could read the responses and then I was golden with a full two way communication, albeit with a highly optimized polling vs an eventstream.

I did play around with some CORS tools for Chrome without any success but I did not go into great detail as I found the solution was the endpoint delivery of the JS app which was my end goal anyway.

I should add that simultaneously changing 5 parameters on 20 devices is likely to be < 100% successful for other reasons.

1 Like

Okay...thanks for the prod back. I figured out most of my mistake. First, I didn't read all of your code (there was a lot of it). Second, I had a typo "method:no-cors" when it should have been "mode: no-cors". Finally, I had that "mode:no-cors" in my header, and it shouldn't have been.

Now, it processes correctly. However, I still get a status ok=false on the return. How are you detecting success?

--EDIT--
The below original post is not correct. See solution identified later in the thread.

Okay...after doing yet more digging, it appears that there is no way to get a response from a GET or POST request. The simple reason is that there is no way to white list a client for a custom app in Hubitat (or, at least I can't find it). Because of that, all the response information is hidden from the script when the browser gets it.

When you do a POST of data from the JS to the Hub and the Hub processes whatever is received, then you have to do a second render back to the JS and include the status code you want to return.

I kept mine simple but it could be more nuanced.

If the client is delivered over the endpoint then CORS does not apply and you don't need to do anything with it.

I have to say thank you (@garyjmilne) for continuing to respond. I was so convinced you were not right, it prompted me to go dig deeper, and I found out you were right, and I had a more complex problem. I'm going to have to go edit my previous solution. This is important, and I suspect others (novices) will miss what is going on.

So, I'll do my best to collect everything that I found and allow the community to correct anything I might get wrong.

First, this link was helpful...a bit confusing on the first 3 reads, but helpful.

Bottom line, the "mode" used within the fetch statement (see code below) has 3 possible settings:

  • no-cor
  • same-origin
  • cors

(There is a fourth, but if I understand it right, it doesn't really apply here.)

The next piece to understand is that the when dealing with a cors request, there is a comparison made between the host name of the computer serving the website, and the hostname used on the fetch request. (This was the confusing part in that link. The terms of the art were not familiar to me and I got really lost.) If they match, all is good and everything works. If they don't match, and you use "no-cor", what happens is that your request goes through, but your browser will not give you the response data. This is what was happening to me. I was getting "response.ok=false" and "response.status=0".

The Problem and the Fix
So, the problem I had is this. I have a static IP setup for my hub served from my router. In my router, I had given my hub a name to match that ip, so that I I could use a simple name to access the hub rather than the ip address. However, when I issued my fetch, I used the ip address, not the name. I don't know if there is something odd going on with the DNS on my router; something odd in the hub; something odd in my browser; or what, but even though the ip and the name were mapped to the same device, the system rejected them as different. Only when I used the exact same host identifier (ip or hostname) did it work.

Wrong way to do this

If, in the web browser you surf to "http:// 192.168.50.1" (example ip). Then in the javascript embedded in your app, issue a command like this:

fetch('http://myhubname/apps/api/100/myRESTAPI?access_token=00000000-0000-0000-0000-000000000000',
{
    method: 'POST',
    mode:  'no-cors',
    headers: {'Content-Type':'application/json'},
    body: JSON.stringify({field: 'data'})
})
.then((response)=>
{
    if(!response.ok)
    {
        console.log('Network response was not ok')
    }
}
.catch(error=>
{    
    console.log(error)
}
)
        
        

This will always fail. It fails because the web page was loaded with the ip address, but the call back to it in the javascript was done by hostname.

The Right Way
The hub does offer the method getLocalApiServerUrl(), however it always seems to return the ip regardless of how the page is called. Again, maybe a setup issue on my side that I can't figure out. For that reason, I recommend using

fetch(window.location.protocol.concat('//').concat(window.location.host)+'/apps/api/100/myRESTAPI?access_token=00000000-0000-0000-0000-000000000000',
{
    method: 'POST',
    mode:  'same-origin',
    headers: {'Content-Type':'application/json'},
    body: JSON.stringify({field: 'data'})
})

in the code above. In that way, from a javascript standpoint, it will always have the name/ip to reference. Note that I also changed the mode to 'same-origin'.

1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.