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
1import json2from insightconnect_plugin_runtime.exceptions import PluginException34def run(self, params={}):5business_object_id = params["business_object_id"]6public_id = params["public_id"]78url = self.connection.base_url() + "/api/V1/getbusinessobject/busobid/{busobid}/publicid/{publicid}".format(9busobid=business_object_id,10publicid=public_id11)1213response = self.connection.session().get(url)14if response.status_code not in range(200, 299):15raise PluginException(16cause="Received HTTP %d status code from Cherwell. Please verify your Cherwell server status and try again.",17assistance="If the issue persists please contact support.",18data=f"{response.status_code}, {response.text}"19)2021try:22response_data = response.json()23except json.decoder.JSONDecodeError:24raise PluginException(25cause="Received an unexpected response from Cherwell ",26assistance="(non-JSON or no response was received).",27data=response.text28)2930return { "success": True, "raw_response": response_data }
Now let's break this example apart and examine it.
Section 1 - Status Code Checking
python
1if response.status_code not in range(200, 299):2raise PluginException(3cause="Received HTTP %d status code from Cherwell. Please verify your Cherwell server status and try again.",4assistance="If the issue persists please contact support.",5data=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
1import json2try:3response_data = response.json()4except json.decoder.JSONDecodeError:5raise PluginException(6preset=PluginException.Preset.INVALID_JSON,7data=response.text8)
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
1raise PluginException(2cause="Okta: Create user failed unexpectedly.",3assistance="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.
- Ensure the user input is correct. Validate user input when necessary.
- 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.
- 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.
- 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
1import json2from insightconnect_plugin_runtime.exceptions import PluginException3...4try:5response_data = response.json()6except json.decoder.JSONDecodeError:7raise PluginException(8preset=PluginException.Preset.INVALID_JSON,9data=response.text10)
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:
1API_KEY = "api_key"2UNAUTHORIZED = "unauthorized"3RATE_LIMIT = "rate_limit"4USERNAME_PASSWORD = "username_password"5NOT_FOUND = "not_found"6SERVER_ERROR = "server_error"7SERVICE_UNAVAILABLE = "service_unavailable"8INVALID_JSON = "invalid_json"9UNKNOWN = "unknown"10BASE64_ENCODE = "base64_encode"11BASE64_DECODE = "base64_decode"
An example using the preset USERNAME_PASSWORD:
python
1from insightconnect_plugin_runtime.exceptions import ConnectionTestException2...3response = requests.get(self.url, auth={'username': 'blah', 'password': 'blah')45# https://developer.atlassian.com/cloud/jira/platform/rest/v2/?utm_source=%2Fcloud%2Fjira%2Fplatform%2Frest%2F&utm_medium=302#error-responses6if response.status_code == 401:7raise 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
1from insightconnect_plugin_runtime.exceptions import ConnectionTestException2...3response = requests.get(self.url, auth={'username': 'blah', 'password': 'blah')4if response.status_code == 404:5raise ConnectionTestException(6cause="Unable to reach Jira instance at: %s." % self.url,7assistance="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
1insightconnect_plugin_runtime.exceptions.LoggedException: Connection test failed!23Unable 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
1import json2from insightconnect_plugin_runtime.exceptions import ConnectionTestException3...4try:5response_data = response.json()6except json.decoder.JSONDecodeError:7raise PluginException(8preset=PluginException.Preset.INVALID_JSON,9data=response.text10)
This example will return the following message when the exception is raised:
sh
1insightconnect_plugin_runtime.exceptions.LoggedException: An error occurred during plugin execution!23Received 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
1from insightconnect_plugin_runtime.exceptions import PluginException2msg = 'The user has sent too many requests in a given amount of time: You have exceeded the rate limit for this service.'3...4raise 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
1insightconnect_plugin_runtime.exceptions.PluginException: An error occurred during plugin execution!23Received 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.