Promises from system.net.httpClient() mishandle TypeErrors in then callbacks

Ignition version 8.1.26

There seems to be a bug in the Promise objects used by system.net.httpClient() async methods. The Promise's then() method requires a callback. That callback can have either one argument or two.
The problem manifests itself when the callback code raises a TypeError.

Incorrect/Confusing error message

Consider the following code that has an intentional TypeError in the callback, which represents an unintentional TypeError.

promise=system.net.httpClient().getAsync("http://google.com/")
def callback(response,error):
	return response['json'] # accidental type error
promise.then(callback).get()

This code raises what seems to be an incorrect error: java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: TypeError: callback() takes exactly 2 arguments (1 given). What? There's something wrong with the callback's number of arguments? This is very confusing.

Callback called twice

I can adjust the callback to be callable with either one or two arguments by giving the error argument a default value.

promise=system.net.httpClient().getAsync("http://google.com/")
def callback(response,error=None):
	system.util.getLogger('httpclient_issue').info('callback called')
	return response['json'] # accidental type error
promise.then(callback).get()

This time I get the correct error message TypeError: 'com.inductiveautomation.ignition.common.script.builtin.http.Response' object is unsubscriptable', but the callback is called twice. You can see that by checking the logs for the httpclient_issue logger and see two entries of 'callback called'.

Root Cause and Possible Solutions

It seems that the Ignition code tries to call the callback with two arguments, and if that call fails with a TypeError, then the callback is called with one argument. For most cases that's fine. The problem is that it gives unexpected results when the callback throws a TypeError.

The solution isn't "Don't have any of your callbacks raise a TypeError". That'd be great, but the TypeError is, of course, unintentional. The solution would be one of these:

  1. Determine how many arguments the callback should be called with before attempting to call it. Then it wouldn't be necessary to try to call the callback twice if the first attempt had the wrong number of arguments.
  2. Keep the current behavior except only re-try the callback with a different number of arguments if you verify that the TypeError is caused by calling the callback with the wrong number of arguments. In the code earlier, the TypeError is caused by an object not being subscriptable, not an incorrect number of arguments.

Last Words

I understand that not many people get affected by this, so I don't expect this bug to be a high priority. It would be nice if it were fixed, but I write this down mostly for anyone else who's wondering why they're getting TypeError: callback() takes exactly 2 arguments (1 given) when they shouldn't. It's because you have a TypeError in your callback and the Ignition code isn't handling it right.

3 Likes

This is great analysis, and from what I remember of the implementation, you're exactly right about why it works the way it does. Any chance you're interested in a job? :slightly_smiling_face:

Both of your proposed fixes would work, in principle. Both would require an unpleasant amount of reliance on Jython's runtime reflection, which I would rather not do. I'll have to think about a good way forward. It is an edge case, to be sure, but an unpleasant one that surfaces in a very confusing way.

1 Like

@PGriffith Thanks for your response.

I'll appreciate any fix for this, but I'm not expecting anything. I know y'all have bigger priorities.

1 Like