Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): clarify auth refresh breaking change on v10 #246

Merged
merged 7 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ Another major release! We have some great improvements in this version but also

(c) **`response.throwForStatus` now only throws an error if the status code is between 400 and 600 (inclusive)**. Before v10, it threw for status >= 300. So if your code rely on that old behavior, you should change your code to check `response.status` explicitly instead of using `response.throwForStatus`.

(d) We now **parse JSON and form-encoded response body by default**. So no more `z.JSON.parse(response.content)`! The parsed object is available as `response.data` (`response.json` will be still available for JSON body but less preferable). Before v10, we only parsed JSON for [manual requests](https://github.com/zapier/zapier-platform/blob/master/packages/cli/README.md#manual-http-requests); parsed JSON and form-encoded body for [shorthand requests](https://github.com/zapier/zapier-platform/blob/master/packages/cli/README.md#shorthand-http-requests). This change could be breaking if you have an `afterResponse` that modifies `response.content`, with the expectation for shorthand requests to pick up on that. In which case, you'll have to replace `response.content = JSON.stringify(parsedOrTransformed)` with `response.data = parsedOrTransformed`.
(d) **Session and OAuth2 refresh now happens AFTER your `afterResponse`**. Before v10, the refresh happens before your `afterResponse`. This is a breaking change if your `afterResponse` captures 401 response status. See [v10 Breaking Change: Auth Refresh](https://github.com/zapier/zapier-platform/blob/master/packages/cli/README.md#v10-breaking-change-auth-refresh) for details.

(e) We rewrote the CLI `zapier init` command. Now the project templates are more up-to-date, with better coding practices. However, **we've removed the following templates**: `babel`, `create`, `github`, `middleware`, `oauth1-tumblr`, `oauth1-twitter`, `onedrive`, `resource`, `rest-hooks`, `trigger`. For trigger/create/search, use `zapier scaffold` command instead. For `babel`, look at `typescript` template and replace the build step with the similar code from https://babeljs.io/setup#installation. For `oauth1`, we now only keep `oauth1-trello` for simplicity. If you ever need to look at the old templates, they're always available in the [example-apps](https://github.com/zapier/zapier-platform/tree/60eaabd04571df30a3c33e4ab5ec4fe0312ad701/example-apps) directory in the repo.
(e) We now **parse JSON and form-encoded response body by default**. So no more `z.JSON.parse(response.content)`! The parsed object is available as `response.data` (`response.json` will be still available for JSON body but less preferable). Before v10, we only parsed JSON for [manual requests](https://github.com/zapier/zapier-platform/blob/master/packages/cli/README.md#manual-http-requests); parsed JSON and form-encoded body for [shorthand requests](https://github.com/zapier/zapier-platform/blob/master/packages/cli/README.md#shorthand-http-requests). This change could be breaking if you have an `afterResponse` that modifies `response.content`, with the expectation for shorthand requests to pick up on that. In which case, you'll have to replace `response.content = JSON.stringify(parsedOrTransformed)` with `response.data = parsedOrTransformed`.

(f) `zapier init` no longer uses the `minimal` template by default. If you don't specify `--template`, **`zapier init` will prompt you interactively**. So if you're using `zapier init` (without any arguments) in CI and expect it to use `minimal` by default, you should replace the command with `zapier init -t minimal`.
(f) We rewrote the CLI `zapier init` command. Now the project templates are more up-to-date, with better coding practices. However, **we've removed the following templates**: `babel`, `create`, `github`, `middleware`, `oauth1-tumblr`, `oauth1-twitter`, `onedrive`, `resource`, `rest-hooks`, `trigger`. For trigger/create/search, use `zapier scaffold` command instead. For `babel`, look at `typescript` template and replace the build step with the similar code from https://babeljs.io/setup#installation. For `oauth1`, we now only keep `oauth1-trello` for simplicity. If you ever need to look at the old templates, they're always available in the [example-apps](https://github.com/zapier/zapier-platform/tree/60eaabd04571df30a3c33e4ab5ec4fe0312ad701/example-apps) directory in the repo.

(g) `zapier init` no longer uses the `minimal` template by default. If you don't specify `--template`, **`zapier init` will prompt you interactively**. So if you're using `zapier init` (without any arguments) in CI and expect it to use `minimal` by default, you should replace the command with `zapier init -t minimal`.

See below for a detailed changelog (**:exclamation: denotes a breaking change**):

Expand All @@ -30,6 +32,7 @@ See below for a detailed changelog (**:exclamation: denotes a breaking change**)

* :exclamation: Integrations now run on Node.js 12!
* :exclamation: `z.request` now always calls `response.throwForStatus` via a middleware by default ([#210](https://github.com/zapier/zapier-platform/pull/210))
* :exclamation: Session and OAuth2 refresh now happens AFTER your `afterResponse` ([#210](https://github.com/zapier/zapier-platform/pull/210))
* :exclamation: `response.throwForStatus` now only throws for 400 <= status <= 600 ([#192](https://github.com/zapier/zapier-platform/pull/192))
* :exclamation: Introduce `response.data` with support for form-urlencoded and custom parsing ([#211](https://github.com/zapier/zapier-platform/pull/211))
* :bug: Don't log request body when it's streaming data ([#214](https://github.com/zapier/zapier-platform/pull/214))
Expand Down
86 changes: 80 additions & 6 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,10 @@
<li><a href="#error-handling">Error Handling</a><ul>
<li><a href="#general-errors">General Errors</a></li>
<li><a href="#halting-execution">Halting Execution</a></li>
<li><a href="#stale-authentication-credentials">Stale Authentication Credentials</a></li>
<li><a href="#stale-authentication-credentials">Stale Authentication Credentials</a><ul>
<li><a href="#v10-breaking-change-auth-refresh">v10 Breaking Change: Auth Refresh</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#testing">Testing</a><ul>
Expand Down Expand Up @@ -537,7 +540,10 @@ <h2 id="table-of-contents">Table of Contents</h2>
<li><a href="#error-handling">Error Handling</a><ul>
<li><a href="#general-errors">General Errors</a></li>
<li><a href="#halting-execution">Halting Execution</a></li>
<li><a href="#stale-authentication-credentials">Stale Authentication Credentials</a></li>
<li><a href="#stale-authentication-credentials">Stale Authentication Credentials</a><ul>
<li><a href="#v10-breaking-change-auth-refresh">v10 Breaking Change: Auth Refresh</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#testing">Testing</a><ul>
Expand Down Expand Up @@ -3011,7 +3017,7 @@ <h3 id="bundletargeturl"><code>bundle.targetUrl</code></h3>
<div class="col-md-5 col-sm-12 col-height docs-primary">
<blockquote>
<p><code>bundle.targetUrl</code> is only available in the <code>performSubscribe</code> and <code>performUnsubscribe</code> methods for webhooks.</p>
</blockquote><p>This the URL to which you should send hook data. It&apos;ll look something like <code>https://hooks.zapier.com/1234/abcd</code>. We provide it so you can make a POST request to your server.Your server should store this URL and use is as a destination when there&apos;s new data to report.</p><p>Read more in the <a href="https://github.com/zapier/zapier-platform/blob/master/example-apps/rest-hooks/triggers/recipe.js">REST hook example</a>.</p>
</blockquote><p>This the URL to which you should send hook data. It&apos;ll look something like <code>https://hooks.zapier.com/1234/abcd</code>. We provide it so you can make a POST request to your server. Your server should store this URL and use is as a destination when there&apos;s new data to report.</p><p>Read more in the <a href="https://github.com/zapier/zapier-platform/blob/master/example-apps/rest-hooks/triggers/recipe.js">REST hook example</a>.</p>
</div>
<div class="col-md-7 col-sm-12 col-height is-empty docs-code">

Expand Down Expand Up @@ -4033,15 +4039,83 @@ <h3 id="stale-authentication-credentials">Stale Authentication Credentials</h3>
provides a mechanism to notify users of expired credentials. With the
<code>ExpiredAuthError</code>, the current operation is interrupted, the Zap is turned off
(to prevent more calls with expired credentials), and a predefined email is sent
out informing the user to refresh the credentials.</p><p>Example: <code>throw new z.errors.ExpiredAuthError(&apos;Your message.&apos;);</code></p><p>For apps that use OAuth2 + refresh or Session Auth, you can use the
<code>RefreshAuthError</code>. This will signal Zapier to refresh the credentials and then
repeat the failed operation.</p><p>Example: <code>throw new z.errors.RefreshAuthError();</code></p>
out informing the user to refresh the credentials.</p><p>Example: <code>throw new z.errors.ExpiredAuthError(&apos;Your message.&apos;);</code></p><p>For apps that use OAuth2 + refresh or Session Auth, the core injects a built-in
<code>afterResponse</code> middleware that throws an error when the response status is 401.
The error will signal Zapier to refresh the credentials and then repeat the
failed operation. For some cases, e.g, your server doesn&apos;t use the 401 status
for auth refresh, you may have to throw the <code>RefreshAuthError</code> on your own,
which will also signal Zapier to refresh the credentials.</p><p>Example: <code>throw new z.errors.RefreshAuthError();</code></p>
</div>
<div class="col-md-7 col-sm-12 col-height is-empty docs-code">

</div>
</div>
</div><div class="row">
<div class="row-height">
<div class="col-md-5 col-sm-12 col-height docs-primary">
<h4 id="v10-breaking-change-auth-refresh">v10 Breaking Change: Auth Refresh</h4>
</div>
<div class="col-md-7 col-sm-12 col-height is-empty docs-code">

</div>
</div>
</div><div class="row">
<div class="row-height">
<div class="col-md-5 col-sm-12 col-height docs-primary">
<p>A breaking change on v10+ is that the built-in <code>afterResponse</code> middleware the
handles auth refresh is changed to happen AFTER your app&apos;s <code>afterResponse</code>. On
v9 and older, it happens before your app&apos;s <code>afterResponse</code>. So it will break if
your <code>afterReponse</code> does something like:</p>
</div>
<div class="col-md-7 col-sm-12 col-height docs-code">
<pre><code class="lang-js"><span class="hljs-comment">// Auth refresh will stop working on v10 this way!</span>
<span class="hljs-keyword">const</span> yourAfterResponse = <span class="hljs-function">(<span class="hljs-params">resp</span>) =&gt;</span> {
<span class="hljs-keyword">if</span> (resp.status !== <span class="hljs-number">200</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">&apos;hi&apos;</span>);
}
<span class="hljs-keyword">return</span> resp;
};
</code></pre>
</div>
</div>
</div><div class="row">
<div class="row-height">
<div class="col-md-5 col-sm-12 col-height docs-primary">
<p>This is because on v10 the <code>throw new Error(&apos;hi&apos;)</code> line will take precedence
over the built-in middleware that does auth refresh. One way to fix is to let
the 401 response fall back to the built-in middleware that does the auth
refresh:</p>
</div>
<div class="col-md-7 col-sm-12 col-height docs-code">
<pre><code class="lang-js"><span class="hljs-keyword">const</span> yourAfterResponse = <span class="hljs-function">(<span class="hljs-params">resp</span>) =&gt;</span> {
<span class="hljs-keyword">if</span> (resp.status !== <span class="hljs-number">200</span> &amp;&amp; resp.status !== <span class="hljs-number">401</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">&apos;hi&apos;</span>);
}
<span class="hljs-keyword">return</span> resp;
};
</code></pre>
</div>
</div>
</div><div class="row">
<div class="row-height">
<div class="col-md-5 col-sm-12 col-height docs-primary">
<p>Another way to fix is to handle the 401 response yourself by throwing a
<code>RefreshAuthError</code>:</p>
</div>
<div class="col-md-7 col-sm-12 col-height docs-code">
<pre><code class="lang-js"><span class="hljs-keyword">const</span> yourAfterResponse = <span class="hljs-function">(<span class="hljs-params">resp</span>) =&gt;</span> {
<span class="hljs-keyword">if</span> (resp.status === <span class="hljs-number">401</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> z.errors.RefreshAuthError();
}
<span class="hljs-keyword">if</span> (resp.status !== <span class="hljs-number">200</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">&apos;hi&apos;</span>);
}
<span class="hljs-keyword">return</span> resp;
};
</code></pre>
</div>
</div>
</div><div class="row">
<div class="row-height">
<div class="col-md-5 col-sm-12 col-height docs-primary">
<h2 id="testing">Testing</h2>
Expand Down
66 changes: 57 additions & 9 deletions packages/cli/README-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,11 +510,11 @@ You can find more details on the definition for each by looking at the [Trigger

Each of the 3 types of function expects a certain type of object. As of core v1.0.11, there are automated checks to let you know when you're trying to pass the wrong type back. There's more info in each relevant `post_X` section of the [v2 docs](https://zapier.com/developer/documentation/v2/scripting/#available-methods). For reference, each expects:

| Method | Return Type | Notes |
| --- | --- | --- |
| Trigger | Array | 0 or more objects that will be passed to the [deduper](https://zapier.com/developer/documentation/v2/deduplication/) |
| Search | Array | 0 or more objects. If len > 0, put the best match first |
| Action | Object | Return values are evaluated by [`isPlainObject`](https://lodash.com/docs#isPlainObject) |
| Method | Return Type | Notes |
|---------|-------------|----------------------------------------------------------------------------------------------------------------------|
| Trigger | Array | 0 or more objects that will be passed to the [deduper](https://zapier.com/developer/documentation/v2/deduplication/) |
| Search | Array | 0 or more objects. If len > 0, put the best match first |
| Action | Object | Return values are evaluated by [`isPlainObject`](https://lodash.com/docs#isPlainObject) |

## Input Fields

Expand Down Expand Up @@ -847,7 +847,7 @@ module.exports = {

> `bundle.targetUrl` is only available in the `performSubscribe` and `performUnsubscribe` methods for webhooks.

This the URL to which you should send hook data. It'll look something like `https://hooks.zapier.com/1234/abcd`. We provide it so you can make a POST request to your server.Your server should store this URL and use is as a destination when there's new data to report.
This the URL to which you should send hook data. It'll look something like `https://hooks.zapier.com/1234/abcd`. We provide it so you can make a POST request to your server. Your server should store this URL and use is as a destination when there's new data to report.

Read more in the [REST hook example](https://github.com/zapier/zapier-platform/blob/master/example-apps/rest-hooks/triggers/recipe.js).

Expand Down Expand Up @@ -1301,12 +1301,60 @@ out informing the user to refresh the credentials.

Example: `throw new z.errors.ExpiredAuthError('Your message.');`

For apps that use OAuth2 + refresh or Session Auth, you can use the
`RefreshAuthError`. This will signal Zapier to refresh the credentials and then
repeat the failed operation.
For apps that use OAuth2 + refresh or Session Auth, the core injects a built-in
`afterResponse` middleware that throws an error when the response status is 401.
The error will signal Zapier to refresh the credentials and then repeat the
failed operation. For some cases, e.g, your server doesn't use the 401 status
for auth refresh, you may have to throw the `RefreshAuthError` on your own,
which will also signal Zapier to refresh the credentials.

Example: `throw new z.errors.RefreshAuthError();`

#### v10 Breaking Change: Auth Refresh

A breaking change on v10+ is that the built-in `afterResponse` middleware the
handles auth refresh is changed to happen AFTER your app's `afterResponse`. On
v9 and older, it happens before your app's `afterResponse`. So it will break if
your `afterReponse` does something like:

```js
// Auth refresh will stop working on v10 this way!
const yourAfterResponse = (resp) => {
if (resp.status !== 200) {
throw new Error('hi');
}
return resp;
};
```

This is because on v10 the `throw new Error('hi')` line will take precedence
over the built-in middleware that does auth refresh. One way to fix is to let
the 401 response fall back to the built-in middleware that does the auth
refresh:

```js
const yourAfterResponse = (resp) => {
if (resp.status !== 200 && resp.status !== 401) {
throw new Error('hi');
}
return resp;
};
```

Another way to fix is to handle the 401 response yourself by throwing a
`RefreshAuthError`:

```js
const yourAfterResponse = (resp) => {
if (resp.status === 401) {
throw new z.errors.RefreshAuthError();
}
if (resp.status !== 200) {
throw new Error('hi');
}
return resp;
};
```

## Testing

Expand Down
Loading