Random 400 errors using the system.net.httpClient() GET function

When using the client.get() function with the same parameters, I randomly get a 400 error as a response.

For testing I'm now always sending the same URL, start/end times and user credentials. On the Azure side, there is a failure message that seems line up with the bad GET requests.

14:07:59 [INF] (Aligned.LochEolas.WebApi) BasicAuthentication was not authenticated. Failure message: Missing Authentication header
14:07:59 [INF] (Aligned.LochEolas.WebApi) AuthenticationScheme: BasicAuthentication was challenged.

Is there a way to see exactly what the client.get() function in Ignition is sending out each time? I want to see if it is indeed randomly changing.

# Create the JythonHttpClient.
client = system.net.httpClient()

# Send a GET request.
url = system.tag.readBlocking(["[default]MemoryTags/REST_API/GET/GET_URL"])[0].value + \
TelemetryType_Keyword + \
System_Keyword + \
subSystem_Keyword + \
Equipment_Keyword + \
Point_Keyword + \
Start_Date + \
End_Date

current_username = system.tag.readBlocking(["[default]MemoryTags/REST_API/User"])[0].value
current_password = system.tag.readBlocking(["[default]MemoryTags/REST_API/Password"])[0].value

#system.perspective.print(url)
logger.info(str(url) + " User: " + current_username + " Password: " + str(current_password))

system.tag.writeBlocking(["[default]MemoryTags/REST_API/GET/GET_Error"], 0)
	try:
		response = client.get(url, username=current_username, 
								   password=current_password)
		#system.perspective.print(response)
		#response_stringTest = str(response)
		#logger = system.util.getLogger("myLogger")
		#logger.info(response_stringTest)
	except ValueError:
		#print("GET - ValueError - URL Unknown Pprotocol")
		system.tag.writeBlocking(["[default]MemoryTags/REST_API/GET/GET_Error"], 1)
		logger.info("GET - ValueError - URL Unknown Pprotocol (https)")
		return
	except IOError:
		#print("GET - IOError - URL Not Found")
		system.tag.writeBlocking(["[default]MemoryTags/REST_API/GET/GET_Error"], 2)
		logger.info("GET - IOError - URL Not Found")
		return
	except:
		system.tag.writeBlocking(["[default]MemoryTags/REST_API/GET/GET_Error"], 3)
		logger.info("GET - Unknown Error")
		return

Looks like you could modify ignition.conf and add this as one of the additional parameters:

-Djdk.httpclient.HttpClient.log=all

or maybe just

-Djdk.httpclient.HttpClient.log=headers,requests

See https://docs.oracle.com/en/java/javase/17/core/java-networking.html

1 Like

Your script is a bit odd (it's cut off and some variables aren't present) but I'd also generally recommend:

  1. If you've got a bare except statement...don't do that. Or at least make sure you're logging it. I can't tell if you are, since you cut off that portion of the code.
  2. Storing usernames and passwords in memory tags seems... wrong. Maybe it's not sensitive, but that's still weird.
  3. Is your URL truly generated by just slapping together a bunch of strings? No delimiters or query param separators or anything in between?
  4. Low priority relative to "stop it from doing weird things" but you should also generally prefer to read tags in one operation vs multiple.
# Move this to the project library
client = system.net.httpClient()

baseUrl, current_username, current_password = [qv.value for qv in system.tag.readBlocking([
   "[default]MemoryTags/REST_API/GET/GET_URL"
   "[default]MemoryTags/REST_API/User"
   "[default]MemoryTags/REST_API/Password"
])]

url = baseUrl + "/".join(
   TelemetryType_Keyword,
   System_Keyword,
   subSystem_Keyword,
   Equipment_Keyword,
   Point_Keyword,
   Start_Date,
   End_Date
)

logger.infof("url: %s, user: %s, password: %s", url, current_username, current_password)

system.tag.writeBlocking(["[default]MemoryTags/REST_API/GET/GET_Error"], 0)
try:
   response = client.get(url, username=current_username, password=current_password)
1 Like
  1. I updated the OP, sorry just did not think anyone would need to see the exceptions.
  2. Okay, just something I added to get them from an input screen. Not sure how to fix it. Maybe only read it from the TextField directly? There more screens that have the same TextFields.
  3. Not sure what is wrong with the URL? It works and not sure what you changed.
  4. Yes, I need to make some time, find and fix stuff like that. It works, is not used often and was built up over time but doing one read is better than many. The example is missing commas?

It is in this script in the project library? It only runs once in a GET script and once in at POST script that runs every ten minutes. There is a way to open the connection once (when Ignition starts?) and then the two scripts can use it when needed?

# Move this to the project library
client = system.net.httpClient()

_______________________________________________________________

I was able to get more info out of the Response using the response.getText() function.

At this time it looks like they had added a check on the end date so it would reject anything beyond the current time. They have removed that and so far so good.

try:
    response = client.get(url, username=current_username, 
                               password=current_password)
    logger.info(response.getText())

Not sure if I did it right but these did not seem to add any extra info.
Added parameters to the ignition.conf file, tried stopping and restarting Ignition.

Where are you looking? They would go into the wrapper.log files. Post your additional parameters section from the ignition.conf if they aren't in there.

Trying to implement this but not sure if it's working the way it should. The GET script still works but is it really just creating the client once?

I added a new script in the Project Library called MyHttpClient.

# MyHttpClient.py in Project Library
client = None

def get_http_client():
    global client
    if client is None:
        client = system.net.httpClient()
    return client

Then in the GET script I added a line to import the get_http_client.

# Import the client from the project library
from REST.MyHttpClient import get_http_client

And then updated the client creation instance.

	# Get the httpClient instance
	client = get_http_client()

You don't need all that ceremony.

If you don't need any customization, just declare:

# MyHttpClient.py in Project Library
client = system.net.httpClient()

Then, at use site, if you want to use a shorter reference just rebind it locally:

# Import the client from the project library
client = REST.MyHttpClient.client

So is the idea to only make the client once or something else? Just moving the client = system.net.httpClient() to another script in the Project Library is all you need?

Isn't this just going to do the same as I already had and run client = system.net.httpClient() each time the GET script runs?

If you declare it as a top level variable in the project library, it'll be unconditionally initialized once when the script manager evaluates the project library [1].

Then all use sites that reference that shared project variable are using the same instance in memory.


  1. it'll also get recreated each time you manipulate anything in the project library, but that applies to every other thing you're doing, also ↩ī¸Ž

How is it declared as a top level variable? I thought that was what the global part was doing. I found that in a thread about top-level named objects.

Would you need a client object for GETs and one for POSTs. The GET script seems normal but now the POST keeps failing saying the the URL Not Found when I switch it to use the REST.MyHttpClient.client.

Literally, just declare it. By virtue of not being inside a class or function definition, it's top level, and therefore automatically available.

You don't need a different client for get vs post. Make sure, if you are rebinding client to be something else locally, you're doing it like I did above, using direct assignment. Don't attempt to from X import Y on project library scripts; it causes problems.

Ok, I could leave it in the same script but above the def get(): or any definition.

The POST and GET URLs are different but are sent each time so I thought they should be ok.

The POST and GET scripts are the same as best I can tell for the binding of client. I'd never had that error before and it seemed like maybe after the GET script ran the URL was still the one for a GET when the POST then tried to run.

For the GET.

# Get the httpClient instance
client = REST.MyHttpClient.clients
		response = client.get(url, username=current_username, 
								   password=current_password)

For the POST.

	# Get the httpClient instance
	client = REST.MyHttpClient.client
Response = client.post(url, data=newPOST, username=system.tag.readBlocking(["[default]MemoryTags/REST_API/User"])[0].value,
					password=system.tag.readBlocking(["[default]MemoryTags/REST_API/Password"])[0].value)

Can you share your full script(s) and the exact error messages you're seeing?

The POST is a lot like the GET from the OP. Now it's using the same function as the GET for the client but is now randomly is showing the except IOError: "POST - IOError - URL Not Found"

I've run it many times now and it's ok for now.

	# Create the JythonHttpClient.
	#client = system.net.httpClient()
	
	# Get the httpClient instance
	client = REST.MyHttpClient.client
	
	# Send bulk URL.
	url = system.tag.readBlocking(["[default]MemoryTags/REST_API/POST/POST_Bulk_URL"])[0].value
	
	# Send a POST request.
	system.tag.writeBlocking(["[default]MemoryTags/REST_API/POST/POST_Error"], 0)
	try:
		Response = client.post(url, data=newPOST, username=system.tag.readBlocking(["[default]MemoryTags/REST_API/User"])[0].value,
					password=system.tag.readBlocking(["[default]MemoryTags/REST_API/Password"])[0].value)
	except ValueError:
		#print("POST - ValueError - URL Unknown Pprotocol")
		system.tag.writeBlocking(["[default]MemoryTags/REST_API/POST/POST_Error"], 1)
		if rePOST_Attempt == 0:
					system.db.runNamedQuery("Insert Data POST Log", {"newValue_POST_Code":9903, "newValue_rePOSTed":"false", "newValue_POST_timestamp":system.date.now()})
		logger.error(rePOST_AttemptLogrPrefix + "POST - ValueError - URL Unknown Pprotocol (https)")
		update_post_log()
		return
	except IOError:
		#print("POST - IOError - URL Not Found")
		system.tag.writeBlocking(["[default]MemoryTags/REST_API/POST/POST_Error"], 2)
		if rePOST_Attempt == 0:
					system.db.runNamedQuery("Insert Data POST Log", {"newValue_POST_Code":9904, "newValue_rePOSTed":"false", "newValue_POST_timestamp":system.date.now()})
		logger.error(rePOST_AttemptLogrPrefix + "POST - IOError - URL Not Found")
		update_post_log()
		return
	except:
		system.tag.writeBlocking(["[default]MemoryTags/REST_API/POST/POST_Error"], 3)
		if rePOST_Attempt == 0:
					system.db.runNamedQuery("Insert Data POST Log", {"newValue_POST_Code":9905, "newValue_rePOSTed":"false", "newValue_POST_timestamp":system.date.now()})
		logger.error(rePOST_AttemptLogrPrefix + "POST - Unknown Error")
		update_post_log()
		return

Looking at the logs overnight every 10 minutes the POSTs are good, until 6:40 a.m. 7:10 a.m. and 7:30 a.m. this morning. The site is on satellite internet so it could be that but with the system POSTing for months I've not seen that error until trying to move the client = system.net.httpClient() to another script in the Project Library. Random issues are the worst.

In the past the POST were always ok, even when making many back to back, they just seem to queue up and one by one work their way through. Watching the Currently Running Scripts, each would take about 30 seconds to a minute to run and the CPU usage stays under 20%. The GETs can lock the Zulu Platform processes at 100% if too many are run at the wrong time, with nothing displayed on the Currently Running Scripts page.

I spoke with the guy on our team working on the Azure database. He said the app was set to idle after 20 minutes of inactivity but even after setting it to "Always On" we still see the random IOError when trying to POST.

He said they have also made changes to get more diagnostic info and after the last IOError this morning there was a failed attempt on the server side. It looks like all the same POST information is sent except the "Authentication Header" is missing.

Running it again over night most all the POSTs attempts work fine every ten minutes but there are still 5 or 6 that fail with the IOError and it seems, a missing "Authentication Header". Is there something about having the one httpClient object that requires something with the user name/password or authentication be handled differently?

I found an example of passing the headers each time but that did not help. There was also an example of closing the connection each time but that just caused errors saying the header is restricted. "Exception: restricted header name: "Connection""

# Create the JythonHttpClient.
#client = system.net.httpClient()

# Get the httpClient instance
client = REST.Utilities.client	# Seems to cause 'POST - IOError - URL Not Found' error?

# Encode username and password in Base64 for the Authorization header
authString = User + ':' + Password
encodedAuth = base64.b64encode(authString.encode('utf-8')).decode('utf-8')

# Set the Authorization header directly in the POST request
headers = {
	'Authorization': 'Basic ' + encodedAuth#,
	#'Connection': 'close'  # Ensures that the connection is closed after the request
}

# Send bulk URL.
url = POST_Bulk_URL
#url = "https://locheolas-api-dev.azurewebsites.net/datapoints/bulk"
#REST.Utilities.POST_logger.error("POST url: " + url)

# Send a POST request.
system.tag.writeBlocking(["[default]MemoryTags/REST_API/POST/POST_Error"], 0)
Response = None
try:
	#Response = client.post(url, data=newPOST, username=User, password=Password)
	Response = client.post(url, data=newPOST, headers=headers)

I'm (frankly) deeply suspicious of something about your environment (a network appliance, firewall). system.net.httpClient is a very thin wrapper around Java's built in HTTP client, and there's nothing in there that would inconsistently send requests. I think the move to change the HTTP client initialization is a red herring, or at most it's surfacing some other logic flaw in your script.

If this happens relatively consistently, I'd suggest some network logging on the gateway server, though since you're using HTTPS it'll be hard to get actually useful logs. I'd recommend engaging our support department at this point if you want more "official" help from Inductive Automation, though, again, I really think this is an environmental problem, not an Ignition one.

I already suggested the JDK http client logging but it seems to have gotten lost.

I suspect it will show nothing in Ignition is randomly modifying the request and that troubleshooting can move elsewhere.

1 Like

It could totally be my script but the odd thing to me is that, commenting out client = system.net.httpClient() or client = REST.Utilities.client and using the other is, so far, like flipping a switch. Letting it create the client each time has been rock solid for months. Creating the client once and reusing it will give me the IOError randomly. It has gone almost over night with no IOError but just this morning the server was rebooted and 2 of the 5 or 6 POSTs have had the IOError.

I opened a support case and there is a setting they are thinking of changing that would require a reboot. They have not told me what it is yet but that they think the Azure server is closing the connection at some point and if it closes at the same time the POST from Ignition comes in it fails.

Our Azure guy is telling me that when the POST fails they see that the "Authentication Header" is missing. Others on the team were wondering if something like a firewall was messing with it but then why only when reusing the one client object?

@Kevin.Herron I'll give the JDK http client logging another try.

Thanks for your time.