Error Handling in Integrations

Error handling is crucial to building a successful product integration. Things don’t always work properly, and your goal should be to minimize customer frustration as much as possible when this happens. This style guide aims to enable you, the developer, to write better code that handles errors more effectively and in a self-service-oriented manner, ultimately reducing the need for users to contact support.

Disclaimers

This guide is built around using the Requests library, although the concepts are relatively code-agnostic.

Trust but verify 3rd party API documentation.

Error Messages

All user-facing text should follow proper English grammar/spelling/syntax unless otherwise noted, including error messages. Write the error messages with language that aligns with the product you are building the plugin to integrate with.

When possible, direct users towards potential resolutions to the error and a reminder to test the integration after they resolve the issue

The following are two example error messages - one good, and one bad:

Good: Error: Authentication to the Acme Service failed. Please verify your API key configured in your plugin connection is entered correctly and that your Acme Service API key is valid and try again. If the issue persists, please contact support.

Bad: Failed to authenticate to acne

The "good" message is good because it details the error (authentication), identifies the authentication credentials as the issue, offers remedial self-service steps towards fixing the issue, and instructs the user to contact support if needed.

The "bad" message is bad because it does not user proper English and does not offer anything else. This includes not explaining what could have caused the failure, not offering remedial steps, and not instructing the user to take proper action. This can lead to unreported errors and wasted time.

The Code

This is a general overview of some code which handles errors effectively and efficiently, while at the same time displaying useful, human-readable, messages to the user.

action.py

python
1
import json
2
from insightconnect_plugin_runtime.exceptions import PluginException
3
4
def run(self, params={}):
5
business_object_id = params["business_object_id"]
6
public_id = params["public_id"]
7
8
url = self.connection.base_url() + "/api/V1/getbusinessobject/busobid/{busobid}/publicid/{publicid}".format(
9
busobid=business_object_id,
10
publicid=public_id
11
)
12
13
response = self.connection.session().get(url)
14
if response.status_code not in range(200, 299):
15
raise PluginException(
16
cause="Received HTTP %d status code from Cherwell. Please verify your Cherwell server status and try again.",
17
assistance="If the issue persists please contact support.",
18
data=f"{response.status_code}, {response.text}"
19
)
20
21
try:
22
response_data = response.json()
23
except json.decoder.JSONDecodeError:
24
raise PluginException(
25
cause="Received an unexpected response from Cherwell ",
26
assistance="(non-JSON or no response was received).",
27
data=response.text
28
)
29
30
return { "success": True, "raw_response": response_data }

Now let's break this example apart and examine it.

Section 1 - Status Code Checking

python
1
if response.status_code not in range(200, 299):
2
raise PluginException(
3
cause="Received HTTP %d status code from Cherwell. Please verify your Cherwell server status and try again.",
4
assistance="If the issue persists please contact support.",
5
data=f"{response.status_code} {response.text}"
6
)

This section handles the HTTP status code for the response. A full list of HTTP status code with their corresponding titles and descriptions can be found here.

Not all APIs utilize the proper HTTP status codes, so always utilize the API documentation status codes over any generic list.

This code uses an if statement to verify that the status code is not in the range of 200-299. 200 represents “OK”. If the status code does not fall within that range, then we are going to halt execution of the integration by way of raising a PluginException and explaining what the issue is to the user.

Section 2 - Payload Verification

python
1
import json
2
try:
3
response_data = response.json()
4
except json.decoder.JSONDecodeError:
5
raise PluginException(
6
preset=PluginException.Preset.INVALID_JSON,
7
data=response.text
8
)

This section marks an attempt to obtain a JSON payload from the response. If the payload is not JSON, or it simply does not exist, then this call will fail and throw a JSONDecodeError. This exception, unhandled, will ultimately be useless to the user when viewing logs as it does not contain any sort of information about the response.

You can handle this by informing the user that the response received was in an unexpected format and then print out the text of the response. This will yield the response from the server which will give the user more information about the issue and how they can go about correcting it.

When possible, direct the user to the product documentation that may aid in resolving their issue. The following is an example of guiding the user to documentation in providing a custom JSON object as input to a plugin action:

python
1
raise PluginException(
2
cause="Okta: Create user failed unexpectedly.",
3
assistance="Make sure any provided objects, such as profile or credentials, match the Okta schemas. "
4
"For more details, see https://developer.okta.com/docs/api/resources/users#request-parameters"
5
)

Raising Exceptions

There are two general actions you can take as a developer when handling errors.

When it is not necessary to halt the plugin code, you can log an error but continue execution. You can do this with self.logger.error(...) to supply an error message to the user.

A PluginException will halt plugin code execution. You can do this with raise PluginException(cause="reason", assistance="helpful message") and then supply a cause and assistance message (as detailed in the above section) to the user for possible remediation. You can use this method when a fatal or error scenario has been reached in your code - such as receiving an HTTP 500 error, invalid response, invalid authentication, etc.

Potential Errors

This list is not all-inclusive, so be mindful of the documentation you are using. With that in mind, following this general list and adjusting as necessary for your specific integration should get you (and the user) off to a great start.

  1. Ensure the user input is correct. Validate user input when necessary.
  2. Check HTTP status codes on the response. Use the API documentation and handle all scenarios they list. Customize the error messages according to the documentation. If no documentation exists for them, check for the most common ones. Note: Some APIs provide error messages in their responses when an error occurs. If this is the case, provide that error message back to the user.
  3. Check the response payload type. For example, if you’re expecting a JSON response, but you get a plaintext response or even XML, will your plugin code break? If yes - handle that. Try/except clauses work fantastically for this scenario. Provide the full response text back to the user if possible.
  4. Check if the required data is present. Is all of the data we need present in the response? If not, can we continue execution or is this a fatal error where we need to raise an PluginException? Use your judgement and available documentation here.

Conventions

Standardize on these when possible.

Examples

JSONDecode

python
1
import json
2
from insightconnect_plugin_runtime.exceptions import PluginException
3
...
4
try:
5
response_data = response.json()
6
except json.decoder.JSONDecodeError:
7
raise PluginException(
8
preset=PluginException.Preset.INVALID_JSON,
9
data=response.text
10
)

Connection Exceptions

Provide error messages that are related to the plugin's connection with the ConnectionTestException interface.

The interface provides a list of preset error messages. Each of these presets are tied to a specific cause and assistance message. This list is subject to frequent changes so the code is the best place for the latest list.

Some preset error messages on the list may include:

1
API_KEY = "api_key"
2
UNAUTHORIZED = "unauthorized"
3
RATE_LIMIT = "rate_limit"
4
USERNAME_PASSWORD = "username_password"
5
NOT_FOUND = "not_found"
6
SERVER_ERROR = "server_error"
7
SERVICE_UNAVAILABLE = "service_unavailable"
8
INVALID_JSON = "invalid_json"
9
UNKNOWN = "unknown"
10
BASE64_ENCODE = "base64_encode"
11
BASE64_DECODE = "base64_decode"

An example using the preset USERNAME_PASSWORD:

python
1
from insightconnect_plugin_runtime.exceptions import ConnectionTestException
2
...
3
response = requests.get(self.url, auth={'username': 'blah', 'password': 'blah')
4
5
# https://developer.atlassian.com/cloud/jira/platform/rest/v2/?utm_source=%2Fcloud%2Fjira%2Fplatform%2Frest%2F&utm_medium=302#error-responses
6
if response.status_code == 401:
7
raise ConnectionTestException(preset=ConnectionTestException.Preset.USERNAME_PASSWORD)

This example will return the following message when the exception is raised:

insightconnect_plugin_runtime.exceptions.LoggedException: Connection test failed! Invalid username or password provided. Verify your username and password are correct.

It also includes the ability to write your own via by supplying the cause, assistance, and data arguments.

python
1
from insightconnect_plugin_runtime.exceptions import ConnectionTestException
2
...
3
response = requests.get(self.url, auth={'username': 'blah', 'password': 'blah')
4
if response.status_code == 404:
5
raise ConnectionTestException(
6
cause="Unable to reach Jira instance at: %s." % self.url,
7
assistance="Verify the Jira server at the URL configured in your plugin connection is correct."
8
)

This example will return the following message when the exception is raised:

sh
1
insightconnect_plugin_runtime.exceptions.LoggedException: Connection test failed!
2
3
Unable to reach Jira instance at: https://example.jira.com. Verify the Jira server at the URL configured in your plugin connection is correct.

Another example using the optional data argument to append Response was: < data > at the end. This is good to use when the API provides a helpful message or when something went wrong and you want to provide the unanticipated response to the user.

python
1
import json
2
from insightconnect_plugin_runtime.exceptions import ConnectionTestException
3
...
4
try:
5
response_data = response.json()
6
except json.decoder.JSONDecodeError:
7
raise PluginException(
8
preset=PluginException.Preset.INVALID_JSON,
9
data=response.text
10
)

This example will return the following message when the exception is raised:

sh
1
insightconnect_plugin_runtime.exceptions.LoggedException: An error occurred during plugin execution!
2
3
Received an unexpected response from the server. (non-JSON or no response was received). Response was: { "asdf": asdf }

Plugin Exceptions

You can use the same mechanism in the ConnectionTestExceptions generically outside of the Connection test method via PluginException. It supports the same presets as well.

python
1
from insightconnect_plugin_runtime.exceptions import PluginException
2
msg = 'The user has sent too many requests in a given amount of time: You have exceeded the rate limit for this service.'
3
...
4
raise PluginException(cause='Received an error response from AbuseIPDB.', assistance='Likely too many requests.', data=msg)

This example will return the following message when the exception is raised:

sh
1
insightconnect_plugin_runtime.exceptions.PluginException: An error occurred during plugin execution!
2
3
Received an error response from AbuseIPDB. Likely too many requests. The user has sent too many requests in a given amount of time: You have exceeded the rate limit for this service.