SmartThings Community

httpPost Error Codes


(George Reese) #1

As far as I can tell from the documentation that I have read (and I’m still not very well read), httpPost just has a success closure that it calls.

The API I am working with returns a MOVED _TEMPORARILY (302) with HTML content on a successful authorization (it shares the login POST with the web form).

I can’t quite figure how to handle this. I have:

	log.debug "Authenticating..."
    def uri = "https://private-8a720-timdorr.apiary-mock.com"
    //def uri = "http://portal.vn.teslamotors.com"
    def postBody = ['user_session[email]' : settings.username,
                 'user_session[password]' : settings.password ]
    def params = [
	    uri : "$uri/login",
        contentType: "application/x-www-form-urlencoded",
        body : postBody
    ]
    log.debug "Params -> $params"
	httpPost(params) {
    	response ->
          log.debug "Response Received: Status [$response.status]"
    }

But I’m not getting to the response. I get an error:

“groovyx.net.http.ResponseParseException: Moved Temporarily @ line 47”


(George Reese) #2

If this is true (http://build.smartthings.com/projects/electricvehicle/), then I’m screwed regardless of figuring this out. I need to be able to read cookie values :frowning:


(George Reese) #3

Never mind that, evidently you can :slight_smile:


(Luke) #4

Hi George,

There are a couple issues here that need to be addressed. First, concerning header parsing, it looks like org.apache.http.Header isn’t whitelisted in our sandbox at the moment, sorry about that. I’ve added it now, so after our next production deploy (by the end of the week), you should be be able to do either this (which should already work):

response.headerIterator('Set-Cookie').each {
  log.debug "Set-Cookie: ${it.value}"
}

or this, which I like better (but appears to be functionally equivalent):

response.getHeaders('Set-Cookie').each {
  log.debug "Set-Cookie: ${it.value}"
}

Regarding the exception you’re seeing (groovyx.net.http.ResponseParseException: Moved Temporarily), in our server logs I can see the true culprit:

Caused by: java.lang.IllegalArgumentException: Response does not have a content-type header

curl gives a more verbose reading of the response:

curl -v -X POST -H 'Content-Type: application/x-www-form-urlencoded' -d 'user_session[email]=fakeusername&user_session[password]=fakepassword' https://private-8a720-timdorr.apiary-mock.com/login

< HTTP/1.1 302 Moved Temporarily
< Date: Wed, 23 Apr 2014 18:03:46 GMT
< Location: https://portal.vn.teslamotors.com/
< Set-Cookie: user_credentials=x; path=/; expires=Fri, 03-May-2013 03:01:54 GMT; secure; HttpOnly
< X-Apiary-Ratelimit-Limit: 120
< X-Apiary-Ratelimit-Remaining: 118
< X-Apiary-Transaction-Id: 53580082a20c9f02000003f2
< Content-Length: 18
< Connection: keep-alive
<
Dummy Welcome Page

The HTTP response being sent has a body with content, but no Content-Type header, so the HTTP library doesn’t know how to parse the body.

Is private-8a720-timdorr.apiary-mock.com a server under your control/influence? If so, adding the following header to the response would probably fix the issue:

Content-Type: text/html

If not, I’ll have to look for a different workaround, so just let me know if this is/isn’t possible.

Thanks,
Luke


(George Reese) #5

Thanks! I don’t control the mock API endpoint. Not sure why it doesn’t set a content type, but the real API endpoint does.


(Luke) #6

I’ve found a way to force our HTTP client to be more forgiving on response Content-Type headers (assume text/plain if there is none explicitly set, basically). I need to do some internal testing to be sure it doesn’t introduce any regressions, but I’m hopeful we can make use of it going forward.

Also, concerning headers, I missed an even better method of header parsing that’s available right now:

response.headers.each {
  log.debug "header ${it.name}: ${it.value}"
}

Or, if you know the header name already:

response.headers['Set-Cookie']

(George Reese) #7

Thanks!


(Luke) #8

After digging around a bit deeper in our HTTP library’s source code, there is a solution you should be able to use right now:

def params = [
  uri: "$uri/login",
  requestContentType: "application/x-www-form-urlencoded",
  body: postBody
]

It turns out that there is a distinction between contentType and requestContentType. contentType is converted into Content-Type and Accept headers, while requestContentType is converted into only a Content-Type header. Also, omitting contentType entirely sets Accept to be /, which is what you want when the server’s response has no Content-Type set.


(George Reese) #9

Thanks!

On a semi-related note, I think there needs to be an ability to define closures to respond to different kinds of error conditions, because status handling is very important in any REST API.

For example, the issue I am running into right now is the fact that, in the Tesla API, it will respond with a 503 the first time you make any call to get data about a specific car. The reason for this is that it has to wake up the car prior to querying it for its state. This can obviously take as long as a minute because of all the middle-devices (ST app<->ST hub<->ST cloud<->Tesla cloud<->car). Without being able to introduce a 503 handler, it just looks like the Thing in the ST app is broken.

On the flip side, I’m not entirely sure what to do if I get a 503 :). I don’t know of a way to force the ST app to indicate that the device is “waiting” and you can’t “sleep” inside a device type like you can a SmartApp.

Thoughts on either or both issues?


(Luke) #10

If you get a 503 response right away but just need to wait a minute (or so) before trying again, and you were in a SmartApp, you could implement logic something like this:

def wakeUpCar() {

  try {

    httpGet(uri: 'http://website.com/foo') { response ->
      log.debug "Response status: ${response.status}"
    }
  } catch (groovyx.net.http.HttpResponseException e) {
    def status = e.response.status
    if (status == 503) {
      // Call getCarData in 90 seconds
      runIn(90, "getCarData", [overwrite: false])
    }
  } 
}

def getCarData() {
  try {

    httpGet(uri: 'http://website.com/foo') { response ->
      log.debug "Response status: ${response.status}"
    }
  } catch (groovyx.net.http.HttpResponseException e) {
    def status = e.response.status
    // Wait long enough to make this code unreachable.
  }
}

This would only function within a SmartApp, however, as our scheduling code hasn’t been ported over to DeviceTypes yet.

Since there’s not a way to do this in a DeviceType, you’re going to need to reorganize your code into what we call a “service manager” SmartApp, along with an accompanying DeviceType that is probably a bit simpler than your current DeviceType; that’s the pattern we follow for our Cloud-Connected Devices (DropCam, WeMo, Sonos, etc), as it allows for things like multi-page device setup and discovery scenarios that require the user to wait for a minute or more, which you may need if the initial GET /vehicles call gives you a 503. If you’re in our IDE and click “Browse SmartApps”, you can check out DropCam (Connect) and Wemo (Connect) source code to get a taste of what that would be like. You would use a SmartApp to manage the collection of Tesla user credentials, then once you’ve received that, you can issue a HTTP request to get a list of Teslas (GET /vehicles). The SmartApp can be configured with a “refreshInterval” that tells our mobile apps to keep asking for a particular page in the SmartApp, and once a response finally comes back from the Tesla API, your page will allow the user to confirm creation of your Tesla device(s), and you will create a child device from within the SmartApp which has all the information it needs to function. The Tesla DeviceType would then mostly delegate to the parent on lock and unlock commands (and handle tile rendering for our “Things” view, in addition to Capability declarations, which allow other SmartApps to use the Tesla anywhere the Lock capability is asked for). It might end up being something like the following (this is a skeleton and requires implementation details):

SmartApp: Tesla (Connect)

preferences {
  page(name: "loginToTesla", title: "Tesla", nextPage: "listAvailableTeslas") {
    input("username", "text", title: "Username", description: "Your TeslaMotors.com username")
    input("password", "password", title: "Password", description: "Your TeslaMotors.com password")
  }
  page(name: "listAvailableTeslas", title: "Tesla")
}

def listAvailableTeslas() {
  // Make call to Tesla API to discover available Tesla vehicles.
  // Handle 503 response and reissue request if necessary.
  // Store them in state.vehiclesDiscovered, which will make them show up on this page.
  getVehicles()

  def vehiclesDiscovered = state.vehiclesDiscovered

  dynamicPage(name: "listAvailableTeslas", refreshInterval: 5) {
    section("Select a vehicle...") {
      input "selectedVehicles", "enum", required:false, title:"Select Tesla Vehicles \n(${vehiclesDiscovered.size() ?: 0} found)", multiple:true, options: vehiclesDiscovered
    }
  }
}

def installed() {
  selectedVehicles?.each { vehicle ->
    // Vehicle should have both the VIN and vehicle id, so you can possibly use either for our DNI (3rd argument, a unique Device Network Identifier)
    addChildDevice("reese", "Tesla", vehicle.vin, null, [label: "My Tesla", data: ["vin": vehicle.vin, "vehicleId": vehicle.vehicle_id]])
  }
}

def lock(vehicleId) {
  // Make call to Tesla API to lock vehicle
}

def unlock(vehicleId) {
  // Make call to Tesla API to unlock vehicle
} 

DeviceType: Tesla (needs a namespace of “reese” and name of “Tesla” for the SmartApp above to be able to find it)

def lock() {
  parent.lock(device.currentValue("vehicleId"))
}

def unlock() {
  parent.unlock(device.currentValue("vehicleId"))
}

(George Reese) #11

Thanks, this was very helpful. I’ve got a bit of work to do :slight_smile:


(Minollo) #12

> This would only function within a SmartApp, however, as our scheduling code hasn’t been ported over to DeviceTypes yet.

…I like the “yet” part; there is hope!


(George Reese) #13

I am being told:

java.lang.RuntimeException: Metadata Error: input() can only be invoked inside a section definition @ line 30


(C Chen) #14

Putting a ‘section’ around the two inputs inside the first ‘page’ seems to work around the error.


#15

I’m getting a BAD REQUEST response when calling Twilio’s REST API. Twilio apparently describes the error in the response body, https://www.twilio.com/docs/errors, but I can’t retrieve it because of the success-only approach to httpPost().

What other options do I have to see that response body?


(Allen Jackson) #16

Is anyone still working on Tesla integration with Smartthings? I tried to install the App from the Smartthings github repository, but can’t get past the login request. Stops here each time:

log.error "login result false"
    return dynamicPage(name: "selectCars", title: "Tesla", install:false, uninstall:true, nextPage:"") {
		section("") {
			paragraph "Please check your username and password"

Does anyone want to reignite this effort? Any suggestions to get this working? I am an old school coder and just not quite up to speed on Groovy and the Tesla API.