Skip to content

Commit

Permalink
feat: Adds async reactor documentation (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhudec authored Feb 21, 2023
1 parent c6f7014 commit 4fb1ded
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 36 deletions.
21 changes: 5 additions & 16 deletions docs/api/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func main() {
### Response

| Attribute | Type | Description |
| ------------------- | --------- | --------------------------------------------------------------------------- |
|---------------------|-----------|-----------------------------------------------------------------------------|
| `title` | _string_ | A short, human-readable summary of the problem |
| `detail` | _string_ | A human-readable explanation specific to this occurrence of the problem |
| `errors.{property}` | _array_ | An array of human readable error messages returned per request `{property}` |
Expand All @@ -123,7 +123,7 @@ func main() {
### Error Codes

| Error Code | Meaning |
| ---------- | ---------------------------------------------------------------- |
|------------|------------------------------------------------------------------|
| `400` | Invalid request body |
| `401` | A missing or invalid `BT-API-KEY` was provided |
| `403` | The provided `BT-API-KEY` does not have the required permissions |
Expand All @@ -134,17 +134,6 @@ func main() {

## Reactor Errors

When calling `POST */react` endpoints, vendor-specific errors are translated to the same
response schema as Basis Theory [Errors](#response). Additional response codes may be returned
on calls to `POST */react` mapped from vendor-specific errors.

### Reactor Error Codes

| Error Code | Meaning | Common Scenarios |
| ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Bad Request | <ul><li>Missing or invalid request properties when processing the request with the vendor</li><li>Invalid Reactor Configuration</li></ul> |
| `401` | Authentication Error | <ul><li>Invalid or unknown credentials</li><li>Credentials are valid, but lack permission to complete the operation</li></ul> |
| `402` | Invalid Payment Method | <ul><li>Expired Card</li><li>A test card or bank account was used in a production environment, or vice-versa</li><li>The vendor denied the card or bank account</li></ul> |
| `422` | Unprocessable Entity | <ul><li>Reactor Formula code is not a valid function</li></ul> |
| `429` | Rate Limit Error | <ul><li>The vendor responded with a 429 HTTP response code</li></ul> |
| `500` | Reactor Runtime Error | <ul><li>An unhandled exception occurred</li><li>The vendor responded with a 5XX HTTP response code</li><li>Vendor connection failure</li></ul> |
Failed [Reactors](/docs/concepts/what-are-reactors) will respond with an error that follow the standard error schema described above.
For further details about types of reactor errors, or best practices around handling errors within your reactor code,
see [Reactor Errors](/docs/api/reactors/reactor-errors).
321 changes: 321 additions & 0 deletions docs/api/reactors/reactor-errors.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
---
title: Reactor Errors
---

import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

Errors that occur when [invoking a reactor](/docs/api/reactors#invoke-a-reactor) will be formatted according to
the standard Basis Theory [Error Response](/docs/api/errors). The contents of each error will vary based on the failure scenario,
and there are several standard error codes that are possible that can be used to determine the cause of the error.

## Reactor Error Codes

| Code | Meaning | Common Scenarios |
|-------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `400` | Bad Request | <ul><li>Missing or invalid `args` on the request</li><li>Invalid Reactor Configuration</li></ul> |
| `401` | Authentication Error | <ul><li>Invalid or unknown credentials</li><li>Credentials are valid, but lack permission to complete the operation</li></ul> |
| `402` | Invalid Payment Method | <ul><li>Expired Card</li><li>A test card or bank account was used in a production environment, or vice-versa</li><li>An external API denied the card or bank account</li></ul> |
| `422` | Unprocessable Entity | <ul><li>Reactor Formula code is not a valid function</li></ul> |
| `429` | Rate Limit Error | <ul><li>An external API responded with a 429 HTTP response code</li></ul> |
| `500` | Reactor Runtime Error | <ul><li>An unhandled exception occurred</li><li>An external API responded with a 5XX HTTP response code</li><li>External API connection failure</li></ul> |

There are a few different root causes for why one of these errors may be returned from a reactor:

1. An error occurred within Basis Theory's reactor execution framework when processing your request. For example,
this can occur if the reactor code is invalid and fails to compile (JavaScript code is validated before being executed) resulting in a `422` error, or if the provided `args`
contain an invalid expression resulting in a `400` error.

2. An error occurred within your reactor code. For example, an HTTP call is made to an external API and they responded with an error,
or a runtime error occurred within the code due to a bug. The following section details best practices when handling errors within reactor code.

## Handling Errors in Reactor Code

The [basistheory-reactor-formulas-sdk-js](https://github.com/Basis-Theory/basistheory-reactor-formulas-sdk-js) package can be referenced
within your reactor code to better control the status codes and messages included on reactor errors.

### Error Types

This package includes several error types that can be thrown from your code, including:

| Type | Status Code | Message |
|-----------------------------|-------------|------------------------|
| `AuthenticationError` | `401` | Authentication Failed |
| `AuthorizationError` | `403` | Forbidden |
| `BadRequestError` | `400` | Bad Request |
| `InvalidPaymentMethodError` | `402` | Invalid Payment Method |
| `RateLimitError` | `429` | Rate Limit Exceeded |
| `ReactorRuntimeError` | `500` | Reactor Runtime Error |

If your reactor code throws any of these standard error types, they will be caught and translated into
the corresponding status code and message specified in the table above.

Any other errors raised from reactor code will result in a `ReactorRuntimeError` containing the original error object
and the response will have status code `500`.

### Error Constructors

Each of the [error types](#error-types) accepts a constructor argument that will be returned within the `errors` block.
The constructor argument for each type supports several error formats, for example:

<Tabs className="bt-tabs" groupId="languages">
<TabItem value="string" label="String">

```javascript

// Throwing an error constructed with a string error message:
const { ReactorRuntimeError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
throw new ReactorRuntimeError('My error message');

// Would result in the following error response:
{
"status": 500,
"message": "Reactor Runtime Error",
"errors": {
"error": ["My error message"]
}
}
```

</TabItem>
<TabItem value="array" label="Array">

```javascript

// Throwing an error constructed with an array:
const { ReactorRuntimeError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
throw new ReactorRuntimeError(["error 1", "error 2"]);

// Would result in the following error response:
{
"status": 500,
"message": "Reactor Runtime Error",
"errors": {
"error": ["error 1", "error 2"]
}
}
```

</TabItem>
<TabItem value="object" label="Object">

```javascript

// Throwing an error constructed with an object:
const { ReactorRuntimeError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
throw new ReactorRuntimeError({
error1: 'description 1',
error2: ['description 2', 'description 3']
});

// Would result in the following error response:
{
"status": 500,
"message": "Reactor Runtime Error",
"errors": {
"error1": ["description 1"],
"error2": ["description 2", "description 3"]
}
}
```

</TabItem>
</Tabs>

### Best Practices

When integrating with an external API within a reactor, we strongly recommend handling all vendor-specific errors
and translating into an appropriate Basis Theory error type.

Some external APIs express error scenarios via HTTP status codes, while others opt to return a generic status code such as `200 OK` while
expressing the error condition within the response body. Each API is unique and error handling may also look very different when using an
SDK vs making a direct HTTP call with a generic HTTP client. For these reasons, error handling logic needs to be
customized for each external API call made from within your reactor code.

In order to make it easier to debug errors returned by a reactor, we strongly encourage you to handle all potential errors
and to craft your reactor responses such that you have sufficient information available in your system to react appropriately to each error scenario.

### Examples

<Tabs>
<TabItem value="stripe" label="Stripe">

```javascript
module.exports = async function (req) {
const stripe = require('stripe')(req.configuration.STRIPE_PRIVATE_API_KEY);
const {
AuthenticationError,
BadRequestError,
InvalidPaymentMethodError,
RateLimitError,
ReactorRuntimeError,
} = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');

try {
const token = await stripe.tokens.create({
// redacted for simplicity
});

return {
raw: token,
};
} catch (err) {
// the stripe sdk throws an error containing a type value
// here we translate from stripe-specific error types into Basis Theory errors
switch (err.type) {
case 'StripeCardError':
throw new InvalidPaymentMethodError();
case 'StripeRateLimitError':
throw new RateLimitError();
case 'StripeAuthenticationError':
throw new AuthenticationError();
case 'StripeInvalidRequestError':
throw new BadRequestError();
default:
throw new ReactorRuntimeError(err);
}
}
};
```
</TabItem>
<TabItem value="adyen" label="Adyen">

```javascript
module.exports = async function (req) {
const {
AuthenticationError,
BadRequestError,
InvalidPaymentMethodError,
InvalidReactorConfigurationError,
RateLimitError,
ReactorRuntimeError,
} = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
const { Client, CheckoutAPI } = require('@adyen/api-library');

const { ADYEN_API_KEY, ADYEN_MERCHANT_ACCOUNT, ADYEN_ENVIRONMENT } =
req.configuration;

const client = new Client({
apiKey: ADYEN_API_KEY,
environment: ADYEN_ENVIRONMENT,
});
const checkout = new CheckoutAPI(client);

let res;
try {
res = await checkout.payments({
// redacted for simplicity
});
} catch (err) {
// Adyen's SDK throws an error that may contain an errorCode
switch (err.errorCode) {
case '101':
case '102':
case '103':
case '129':
case '140':
case '141':
case '153':
throw new InvalidPaymentMethodError(err);
case '159':
throw new RateLimitError(err);
case '180':
case '198':
throw new BadRequestError(err);
case '901':
throw new InvalidReactorConfigurationError(err);
}

// or the error may contain a statusCode
switch (err.statusCode) {
case 401:
case 403:
throw new AuthenticationError(err);
case 400:
case 422:
throw new BadRequestError(err);
case 429:
throw new RateLimitError(err);
default:
throw new ReactorRuntimeError(err);
}
}

// the API call may have also succeeded and returned an error resultCode
switch (res.resultCode) {
case 'Refused':
throw new InvalidPaymentMethodError(res);
case 'Error':
throw new ReactorRuntimeError(res);
default:
return {
raw: res,
};
}
};
```

</TabItem>
<TabItem value="spreedly" label="Spreedly">

```javascript
module.exports = async function (req) {
const fetch = require('node-fetch');
const {
AuthenticationError,
BadRequestError,
InvalidPaymentMethodError,
RateLimitError,
ReactorRuntimeError,
} = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');

const { SPREEDLY_API_ENV_KEY, SPREEDLY_API_ACCESS_KEY } = req.configuration;

const res = await fetch('https://core.spreedly.com/v1/payment_methods.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization:
`Basic ` +
Buffer.from(
`${SPREEDLY_API_ENV_KEY}:${SPREEDLY_API_ACCESS_KEY}`,
'binary'
).toString('base64'),
},
body: JSON.stringify(creationPayload),
});

if (res.status !== 201) {
const response = res.headers
.get('Content-Type')
?.includes('application/json')
? await res.json()
: await res.text();

// the spreedly api responds with an http status code indicating the error
// here we translate from a mixture of response status codes and errors in
// the response body into basis theory errors
switch (res.status) {
case 401:
case 403:
throw new AuthenticationError(response);
case 402:
case 422:
if (response.errors?.find((e) => e.key.startsWith('errors.metadata')))
throw new BadRequestError();
throw new InvalidPaymentMethodError(response);
case 429:
throw new RateLimitError(response);
default:
throw new ReactorRuntimeError(response);
}
}

return {
raw: await res.json(),
};
};
```
</TabItem>
</Tabs>
6 changes: 4 additions & 2 deletions docs/api/reactors/reactor-formulas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,7 @@ Complex nested objects (dot-separated names) are not currently supported within

### Reactor Formula Request Parameters

The `request_parameters` array on a Reactor Formula defines the contract that the `args` property must satisfy on each request when [Invoking a Reactor](/docs/api/reactors#invoke-a-reactor).
The `request_parameters` array on a Reactor Formula defines an optional contract that the `args` property must satisfy on each request when [Invoking a Reactor](/docs/api/reactors#invoke-a-reactor).
Elements of a Reactor Formula's `request_parameters` array have the following schema:

| Attribute | Required | Type | Default | Description |
Expand All @@ -1270,7 +1270,9 @@ Elements of a Reactor Formula's `request_parameters` array have the following sc
Request parameters are intended to define any parameters that will be provided to a Reactor at request-time, and may change across Reactor invocations.
Complex objects properties can be passed within the `args` property to a Reactor, and they can be defined by dot-separating levels of the object hierarchy.

Any `args` property not associated with a request parameter is still forwarded to the reactor. This allows you to provide complete complex objects including arrays, in which no type checking is applied. For instance, if no request parameters are declared, it means you can provide any payload when invoking the reactor.
Any undeclared `args` not matching a request parameter are still forwarded to the reactor. This means that if no request parameters are pre-declared,
then you can provide any payload when invoking the reactor, but the tradeoff is that your reactor formula code must be robust enough
to handle all possible request schemas.

### Reactor Formula Code

Expand Down
Loading

1 comment on commit 4fb1ded

@vercel
Copy link

@vercel vercel bot commented on 4fb1ded Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.