408 Response From Async HTTP Actions Doesn't Bring Data Map?

@chuck.schwer I have a driver that sends all requests through this generic doAction method and handler:

def doAction(type, host, path, def body = []) {
  logger('debug', "doAction($type, $path, $body)")
  def params = [
    uri: "http://" + host,
    path: path,
    timeout: 1
  ]
  if (!body.equals([])) {
    params.contentType = 'application/json'
    if (body != []) params.body = body
  }
  logger('debug', "params: $params")
  try {
    "async${type}"("responseHandler", params)
  }
  catch (e) {
    log.error "HTTP Exception received on ${type}: ${e}"
  }
}

def responseHandler(response, params) {
  logger('debug', "responseHandler(${response.status}, ${params})")
  response.properties.each {
    logger('trace', it)
  }
  if (response.status == 200 && response.json) {
    handleJson(response.json)
  }
  else {
    logger('error', "Request failed with status ${response.status}! Request: ${params}")
  }
}

Note that the request is always put in the "params" map and passed to the async call. To me, that means that it should always be available in the params in the responseHandler, correct?

So, I've been noticing this on occasion.

A 408 comes back (not sure why btw) and the request is missing from params. Params is actually null. Why is that?

Huh, actually now that I'm looking at it a little closer it appears that the params are not coming forward ever. When I wrote this a million years ago I think they were coming through. I'll check the documentation. I may have premature mentioned you.

Yeah, I'm not going crazy. Phew...

https://docs.hubitat.com/index.php?title=Common_Methods_Object#asynchttpPost

So, it would be weird that this block of code would ever produce this output, right?

Even on these successful responses params is null.

Don't you have to put "uri", "path" and "timeout" in quotes? Otherwise, if your path is "some/path/request" won't your second parameter be "some/path/request:some/path/request" instead of "path: some/path/request"

Could it be that your variable and the parameter name are both "path"?

Your callback method won't get your params from your original request though. If you want to send data to the callback method, you have set it in the data:

I don't see a data map in your async request. The params will be used to send the request.

Will be what your callback method received.

Yeah, that is a mistake, it should be some other code. It will come back anytime an exception happens in the async client code, you can get the exception text from response.getErrorMessage(), but only if response.hasError() is true, ie:

if(response.hasError()) {
    log.warn(response.getErrorMessage())
}

Yep, this is correct.

1 Like

3rd time was a charm. I knew I'd find it eventually. :slight_smile:

This hurts to admit but the reality of what happened here is actually worse than me not having passed the data map. I DID pass the data map but I did it in the wrong driver. I have two drivers that are nearly identical and I made changes in the wrong one. I pasted you the code that is being used in the driver I didn't edit. If I had just looked at the code I pasted I would have figured this out. :frowning:

I added the debug code to a driver that wasn't in use... :slow_clap:

As I slowly troubleshoot this problem over the last weeks I have another question that is closely related to this one.

Ryan or Chuck, did I pass the timeout correctly? According to the documentation "timeout" is the amount of seconds for the request (max of 300). For the request to connect? Or for the request to fail without response? Because I am passing 1 second but HE always.... always times out after 2 minutes.

Chuck, is that happening because it does connect but no response is given? If I Postman this sometimes that's what happens. It connects, the server responds to the action and does a thing (it's a relay server so I can see it received and acted on the request) but it just sits forever and no response is given. I will probably have to fix that eventually but for now I have to make sure HE doesn't wait for 2 minutes if the relay server does that.

For reference again here is the request and response methods:

def doAction(type, host, path, def body = []) {
  logger('debug', "doAction($type, $path, $body)")
  def params = [
    uri: "http://" + host,
    path: path,
    timeout: 1
  ]
  if (!body.equals([])) {
    params.contentType = 'application/json'
    if (body != []) params.body = body
  }
  logger('debug', "params: $params")
  try {
    "async${type}"("responseHandler", params, params += [type: type])
  }
  catch (e) {
    log.error "HTTP Exception received on ${type}: ${e}"
  }
}

def responseHandler(response, params) {
  logger('debug', "responseHandler(${response.status}, ${params})")
  response.properties.each {
    logger('trace', it)
  }
  if (response.status == 200 && response.json) {
    handleJson(response.json)
  }
  else {
    logger('error', "Request failed with status ${response.status}! Request: ${params}")
    if (response.hasError()) {
      logger('error', response.getErrorMessage())
    }
  }
}

I've never used a timeout. With an async call, it doesn't really matter, does it? I didn't even know you could pass a timeout with the async calls.

I'm still not quite understanding what you have here:

try {
    "async${type}"("responseHandler", params, params += [type: type])
  }
  catch (e) {
    log.error "HTTP Exception received on ${type}: ${e}"
  }
}

It appears that you're doing both a try/catch and an async call? I would not typically combine the two. I'm not sure that you can. The error would be logged in the callback method, not in your originating method.

Also, you are passing data off to the callback method, why are you ass passing the params? If you want to pass data off the to the callback method, it has to be in a map. So, I don't think that params+[type:type] is going to work.

You can try/catch any block of code. It will cover things like passing incorrect parameters. Since the request is built in many parts of code it's good practice to cover all the bases. A catch block is required after a try so that if the executed code in the try fails it has something to fall back on. This try/catch is fine but thanks for the observation. In your quoted block you have an extra bracket which makes it look a little funky and maybe that's what is hanging you up?

I pass the map twice because I want the response handler to know what the original request was. This also isn't a problem. That third parameter (as you pointed out above) is just passed through to the handler. In groovy (this is a fun thing) maps have the += operator implemented. This means that:

map += [type: "httpPost"] is the same as

map = map + [type: "httpPost"] is the same as

[uri: "http://blahblah", path: "/blahblah", type: "blah"] = [uri: "http://blahblah", path: "/blahblah"] + [type: "httpPost"]

It's just shorter syntax. You can see it in the print outs. It's okay too. Good observations though. I think this one is going to be a question for @chuck.schwer if nobody sees a problem with how I setup timeout.

I know that part. But the Async call doesn't get a response, the callback method does. So, how will it know if it's succeeded?

But you are adding [type:type], so if type == POST, wouldn't that add [POST:POST] to the map? Wouldn't you want to add the map ["type":type] How does it know you want to substitute for the value of the map but not the key? Both the key and the value are "type".
(and I am honestly asking because I just fought with a map for an hour this morning and almost threw my hubitat out the window. Or more accurately, a map within a JSON. I don't know if it's what I'm supposed to do but the only way I could get the value was to do: myJson.data.key[0]. Otherwise the value kept coming out an array. I swear i felt my blood pressure go up 20 points.)

I'll give @codahq a break and handle this one. :slight_smile: Groovy is a bit weird here compared to other languages. If your key is a string and doesn't contain special characters, it by default assumes a "convenience notation" that allows you to type them without a string. So, [a:'my value'] is the same as ['a':'my value']. If you do have (in the previous example) a variable a and want to use its value instead, you can force that interpretation with parentheses: [(a):'my value'].

Okay...now you're just screwing with me, right? That answer can be boiled down to Groovy saying, "Because I said so." LMAO How are people who aren't programmers supposed to learn this stuff?!?!
:rage:

And hey, James Strachan, designer of Groovy,
image
LMAO. (yes, it's been that kind of day)

But thank you for taking the time to explain it. Again, one of those things that make total sense once you know it.