HTTP POST json + Authorization

I'm trying to send a a HTTP POST with JSON. I first tried RM, but since I need to include Authorization in the header, I believe RM will not be the right solution. Stumbled across some other posts and tried @ogiewon's HTTP Momentary Switch, but didn't have any luck.

Here's what I'm trying to POST. It works fine in the Postman program.

POST /v1.1/109.3/1912345/Commands/ HTTP/1.1
Host: api.abc123.com
Authorization: Basic xyzxyz123xyzxyz123xyzxyz123xyzxyz123=
Content-Type: application/json

{

    "DeviceID": 11111,
    "Type": "Action"

}

Any suggestions on the simplest way to run this in HE?

WARNING, I know jack squat about HTML or jason.
Anywho check this thread, right on topic. Something about a token, but without any grass

Howdy. I read through that thread and it looks like maybe you are referring to a case where a POST is sent to the HE. I’m needing to send a POST from the HE to a remote HTTPS.

Ah, okay. There are many really skilled members here , try searching the forum under coding questions tags, and you should find someone who speaks your "language"

Try this

1 Like

I have made a quick tweak to add "Authorization" to the header that hopefully makes sense to you when you see it in the code. I have not tested this change whatsoever. Give it a try and let us know this this helps.

/**
 *  HTTP Momentary Switch
 *
 *  Copyright 2018 Daniel Ogorchock
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who             What
 *    ----        ---             ----
 *    2018-02-18  Dan Ogorchock   Original Creation
 *    2019-10-11  Stephan Hackett Added ContentType and Body inputs(very limited functionality)
 *    2020-01-25  Dan Ogorchock   Added ImportURL Metadata
 *    2020-10-06  Dan Ogorchock   Attempt to add Authroization Header for specific user request
 * 
 */

metadata {
    definition (name: "HTTP Momentary Switch", namespace: "ogiewon", author: "Dan Ogorchock", importUrl: "https://raw.githubusercontent.com/ogiewon/Hubitat/master/Drivers/http-momentary-switch.src/http-momentary-switch.groovy") {
        capability "Switch"
        capability "Momentary"
    }

    preferences {
        input(name: "deviceIP", type: "string", title:"Device IP Address", description: "Enter IP Address of your HTTP server", required: true, displayDuringSetup: true)
        input(name: "devicePort", type: "string", title:"Device Port", description: "Enter Port of your HTTP server (defaults to 80)", defaultValue: "80", required: false, displayDuringSetup: true)
        input(name: "authorization", type: "string", title:"Authorization", description: "Enter the Authorization string", displayDuringSetup: true)
        input(name: "devicePath", type: "string", title:"URL Path", description: "Rest of the URL, include forward slash.", displayDuringSetup: true)
        input(name: "deviceMethod", type: "enum", title: "POST, GET, or PUT", options: ["POST","GET","PUT"], defaultValue: "POST", required: true, displayDuringSetup: true)
        input(name: "deviceContent", type: "enum", title: "Content-Type", options: getCtype(), defaultValue: "application/x-www-form-urlencoded", required: true, displayDuringSetup: true)
        input(name: "deviceBody", type: "string", title:"Body", description: "Body of message", displayDuringSetup: true)
    }
}

def parse(String description) {
    log.debug(description)
}

def getCtype() {
    def cType = []
    cType = ["application/x-www-form-urlencoded","application/json"]
}

def push() {
    //toggle the switch to generate events for anything that is subscribed
    sendEvent(name: "switch", value: "on", isStateChange: true)
    runIn(1, toggleOff)
    //sendEvent(name: "switch", value: "off", isStateChange: true)
    runCmd(devicePath, deviceMethod)
}

def toggleOff() {
    sendEvent(name: "switch", value: "off", isStateChange: true)
}

def on() {
    push()
}

def off() {
    push()
}

def runCmd(String varCommand, String method) {
    def localDevicePort = (devicePort==null) ? "80" : devicePort
    def path = varCommand 
    def body = ""
    if(deviceBody) body = deviceBody
    def headers = [:] 
    headers.put("HOST", "${deviceIP}:${localDevicePort}")
    headers.put("Authorization", authorization)
    headers.put("Content-Type", deviceContent)

    try {
        def hubAction = new hubitat.device.HubAction(
            method: method,
            path: path,
            body: body,
            headers: headers
            )
        log.debug hubAction
        return hubAction
    }
    catch (Exception e) {
        log.debug "runCmd hit exception ${e} on ${hubAction}"
    }  
}
1 Like

Thanks @ogiewon! I tried something similar with your driver, but I'm far from a developer, so much rather your version. In the Auth field, I'm adding the "Basic 12312312ew8rsdfsdfsdfsdf=" string (base64 for the username:pw). Still not having any luck..

Log looks like... (with stuff changed for privacy)

POST /v1.1/109.3/111111/Commands/ HTTP/1.1
Accept: /
User-Agent: Linux UPnP/1.0 Hubitat
HOST: api.123.com:80
Authorization: Basic 121223qwewqeqweqeqwewqeqweqweqweqweqwe=
Content-Type: application/json
Content-Length: 39

{"DeviceID":111111,"Type":"Thing"}

Is there any specific syntax for the body? {} 's needed? Spacing?

You mentioned needing to use https, correct? Try changing the port from 80 to 443 in the user settings. Just a guess.

No dice on 443.

Postman code looks like this, no port at all, and it works there. Maybe HTTPS isnt the issue.

POST /v1.1/109.3/11111/Commands/ HTTP/1.1
Host: api.123.com

When I specify port 80 in Postman it still works. This is a real head scratcher.

I like to use postman echo to figure out what the difference is between something that works in postman vs. another client (like Hubitat).

Here's a stripped down version of Basic authorization code from one of my drivers. Enter the username and password and save, then open the Logs window in a separate tab, and then click Configure. You should (hopefully) see a response print.

If not, change the URI in this demo driver to target postman echo, then look at the response print compared to one sent to postman echo from postman and you can see what differs.

/*
*/

metadata
{
    definition(name: "HTTP Post demo", namespace: "tomw", author: "tomw", importUrl: "")
    {
        capability "Configuration"        
    }
}

preferences
{
    section
    {
        input "username", "text", title: "Username (email)", required: true
        input "password", "password", title: "Password", required: true
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
    }
}

def logDebug(msg) 
{
    if (logEnable)
    {
        log.debug(msg)
    }
}

def configure()
{
    try
    {
        def postParams = genParamsPre()
        postParams['body'] = ["DeviceID": 11111, "Type": "Action"]
        httpPostExec(postParams, true)
    }
    catch (Exception e)
    {
        logDebug("configure() failed")
    }
}

def getBaseURI()
{
    return "https://api.abc123.com"
}

def genParamsPre()
{
    def params =
        [
            uri: getBaseURI(),
            headers:
            [
                'Authorization': "Basic " + ("${username}:${password}").bytes.encodeBase64().toString()
            ],
            contentType: 'application/json'
        ]
 
    return params
}

def httpPostExec(params, throwToCaller = false)
{
    logDebug("httpPostExec(${params})")
    
    try
    {
        def result
        httpPost(params)
        { resp ->
            if (resp.data)
            {
                logDebug("resp.data = ${resp.data}")
                result = resp.data
            }
        }
        return result
    }
    catch (Exception e)
    {
        logDebug("httpPostExec() failed: ${e.message}")
        if(throwToCaller)
        {
            throw(e)
        }
    }
}
3 Likes

This is really helpful, thank you. In testing with your code I'm able to see logs that tell me I'm successfully being authorized. Atleast I know that I'm getting to the API and am close..

Now I'm just stuck at "httpPostExec() failed: Bad Request" when hitting the real world API that I'm trying to reach.

Doing the postman-echo test and observing the responses, one thing that jumps out is:

From HE: data:, files:[:], form:[DeviceID:123, Type:Action],
From Postman: "data":{"DeviceID":123,"Type":"Action"},"files":{},"form":{},

Any idea what's driving the data I'm passing being in the 'form' rather than the 'data' ?

It looks like the encoding of the body info is weird for some reason. I suspect that it is "smart" enough to react to different datatypes as input. Without being able to look at the innards of the Hubitat implementation to see how things are actually implemented, I usually just try variations of these two params on the request to find the right magic combination:

EDIT: To be more clear: try different combinations of those settings (json or x-www-form-urlencoded) and try specifying or not those settings. If you share the full postman echo prints, there may be other clues.

I thought the switch over to application/x-www-form-urlencoded was going to do it, since the commands were consistently in the "form" now, but still no luck. Bad Request when I send to my real API. Postman works with the x-www-form-urlencoded to the API.

From Postman:
{"args":{},"data":"","files":{},"form":{"DeviceID":"11111","Type":"Action"},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-5f7cc48f-1adde9ce276f711270eab68c","content-length":"29","authorization":"Basic 111111111111111111111111111111111111111111","cache-control":"no-cache","content-type":"application/x-www-form-urlencoded","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36","postman-token":"0cd75417-c343-7523-c997-c2e3f4407922","accept":"/","origin":"chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop","sec-fetch-site":"none","sec-fetch-mode":"cors","sec-fetch-dest":"empty","accept-encoding":"gzip, deflate, br","accept-language":"en-US,en;q=0.9","cookie":"sails.sid=s%3AtukX6ZXgoaoPPs3N3r-VSZGoVoaFA-gk.vIo1nmRQwX4y9lIl86gI%2F032DbGbR8rQrHn%2FhOAwtkM"},"json":{"DeviceID":"11111","Type":"Action"},"url":"https://postman-echo.com/post"}

From HE:
resp.data = [{"args":{},"data":"","files":{},"form":{"DeviceID":"11111","Type":"Action"},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root:1-5f7cc64e-0db98e204324d1c07c0a64ab,content-length:29,accept:application/x-www-form-urlencoded,authorization:Basic 111111111111111111111111111111111111111111,content-type:application/x-www-form-urlencoded,user-agent:Apache-HttpClient/4.5.2 (Java/1.8.0_181),accept-encoding:gzip,deflate},json:{DeviceID:11111,Type:Action},url:https://postman-echo.com/post}]

It seems like you are getting close! What did you set specifically for contentType and requestContentType for the postman echo trace that you shared? Based on the successful postman request, I'd try these two:

contentType: "application/x-www-form-urlencoded",
requestContentType: "application/json" 

or just

contentType: "application/x-www-form-urlencoded"
(requestContentType not specified)

Snippets were using only:
contentType: "application/x-www-form-urlencoded"

Also tried both on the real API and no luck yet.
contentType: "application/x-www-form-urlencoded",
requestContentType: "application/json"

I have a couple more ideas, but at this point I'm just shooting in the dark. What type of account is required to interact with the server, and could I get access to just poke and try to figure it out? Is there an API reference document?

Mind if I PM you? It’s complicated

Case closed! @tomw's code with requestContentType: "application/json" ultimately did the trick. https://webhook.site was a helpful resource for me to actually see how the POSTs were coming from HE vs. Postman. This was certainly tricky to figure out, but learned alot in the process.

Thank you @tomw, @ogiewon and @Rxich for the help!

4 Likes