fetchによるHTTPリクエストをAbortControllerで中断する

fetchを使ったHTTPリクエストを中断する方法を紹介します。中断後の処理や、複数のHTTPリクエストの一括中断の方法など、実践的なケースにも触れ、正しいエラーハンドリングについて説明します。

2021/10/103 min read
..
hero image

はじめに

AbortController は非同期処理を中断するためのインターフェイスで、Node.js では 15.0.0 から使えるようになりました。 今回は代表的な非同期処理である HTTP リクエストのキャンセルについて説明します。

HTTP でリクエストを送信するには、古くは XMLHttpRequest を使っていましたが、昨今では Promise ベースの Fetch API を使うことが多いと思います。

axios や ky といった HTTP クライアントライブラリの使用率は非常に高いですが、Universal API として 基本的な Fetch API でのキャンセレーションについて説明します。

Fetch API について

モダンブラウザと Deno では Fetch API を標準で利用可能です。Node.js でも、node-fetch があるので、Fetch API は HTTP クライアントとしてユニバーサルに利用できるといって過言ありません。ですので、まずは Fetch API での利用法をしっかり抑えましょう。

Fetch API は第2引数に RequestInit というオブジェクトを受け取ります。インターフェイスは次のとおりです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
declare function fetch(
input: Request | URL | string,
init?: RequestInit
): Promise<Response>
interface RequestInit {
body?: BodyInit | null
cache?: RequestCache
credentials?: RequestCredentials
headers?: HeadersInit
integrity?: string
keepalive?: boolean
method?: string
mode?: RequestMode
redirect?: RequestRedirect
referrer?: string
referrerPolicy?: ReferrerPolicy
signal?: AbortSignal | null
window?: any
}
ts

signal というキーは、 AbortSignal を受け取ります。AbortSignal は AbortController クラスのメンバーです。

AbortController について

AbortController は、非同期処理を中断できるシグナルオブジェクトを含むコントローラーです。コンストラクターからオブジェクトを生成できます。

1
2
3
4
5
6
const controller = new AbortController()
declare class AbortController {
readonly signal: AbortSignal
abort(): void
}
ts

AbortController はシグナルオブジェクトの参照と abort メソッドを持ちます。この signalfetch に渡し、abort メソッドを呼ぶことで、HTTP リクエストを中断できます。

例は Deno 以外の実行環境を想定しています。Deno は 1.10.3 の時点でまだ Fetch API のキャンセレーションが実装されていません。main ブランチにマージされたのでおそらく近日中に利用できると思います。

Top-Level Await 記法を使用しています

1
2
3
4
5
6
7
8
9
10
const url = 'https://google.com'
const controller = new AbortController()
await fetch(url, {
signal: controller.signal
})
setTimeout(() => {
controller.abort()
}, 1000)
ts

上の例では、1000 ミリ秒後に、リクエストを中断します。 UI 上ではボタンのクリックイベントなどに abort 関数の呼び出しをバインドすることで、ユーザー主導のキャンセリングを実現できます。

これで中断はできましたが、次に中断後の処理について考えます。

中断をハンドルするにはいくつかの方法が存在します。それぞれ見ていきましょう。

Fetch API の reject

Fetch API では、次の 2 つのケースで reject が発生すると定義されています。詳しくは仕様書を参照してください。

  • TypeError
  • AbortError

TypeError はネットワークエラーの発生とともにスローされます。例えば、存在しない URL へのリクエストは TypeError が発生します。

1
2
await fetch('https://this-is-not-exist.com')
Uncaught TypeError: error sending request for url (https://this-is-not-exist.com/): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known
ts

そして、もう一つのエラーが AbortError です。これはリクエストの中断とともに発生します。

AbortError を拾うことで、エラー処理をきっちり行うことができます。 また、TypeErrorAbortError を拾い分けることで、ユーザーフレンドリーな通知などが行えます。

1
2
3
4
5
6
7
8
9
try {
await fetch('https://this-is-not-exist.com')
} catch (e) {
if (e.name === 'AbortError') {
// Abort error handling
} else {
// Network error handling
}
}
ts

上の例では tryCatch 文でエラーキャッチをしましたが、もちろん Promisereject 関数からもエラーを拾うことができます。

イベントハンドラーとイベントリスナー

AbortSignal のインターフェイスは次のとおりです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface AbortSignal extends EventTarget {
readonly aborted: boolean
onabort: ((this: AbortSignal, ev: Event) => any) | null
addEventListener<K extends keyof AbortSignalEventMap>(
type: K,
listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void
removeEventListener<K extends keyof AbortSignalEventMap>(
type: K,
listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
options?: boolean | EventListenerOptions
): void
}
ts

AbortSignal には onabort というイベントハンドラがあります。 これに任意の関数をセットすることで、中断時にその関数が呼び出されます。

1
2
const controller = new AbortController()
controller.signal.onabort = () => {}
ts

また、イベントリスナーの typeabort とすることで、同じように中断を監視できます。

1
controller.signal.addEventListener('abort', () => {})
ts

また、読み取り専用プロパティの abortedAbortSignal が中断されたかどうかを表します。

複数の HTTP リクエストを中断する

AboutController は、複数の fetch 関数の呼び出しに渡すことができ、一括で HTTP リクエストを中断できます。

1
2
3
4
5
6
7
8
9
10
const controller = new AbortController()
const { signal } = controller
try {
await Promise.all(
[endpoint1, endpoint2, endpoint3].map((url) => {
fetch(url, { signal })
})
)
} catch (e) {}
ts

また、エラーのキャッチも一括で行えます。

複数回中断させる

AbortController は一度 abort を呼び出すと、 その AbortSignal を参照にしている fetch 関数を再度実行できません。

例えば Vue では次のように書いてしまいがちになります。

1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts">
const controller = new AbortController()
const onCancel = () => {
controller.abort()
}
const onClick = async () => {
await fetch(url, { signal: controller.signal })
}
</script>
html

この例では、AbortController インスタンスは onClick の度に再生成されるわけではないで、中断後 2 回目の HTTP リクエストを行えません。

インスタンスを fetch の度に再設定する必要があるので、次のようにします。

1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts">
let controller
const onCancel = () => {
controller?.abort()
}
const onClick = async () => {
controller = new AbortController()
await fetch(url, { signal: controller.signal })
}
</script>
html

変数のスコープ上、let で宣言しなければならないのが残念ですが、これで fetch の度に新しいインスタンス設定できます。


Edit this page on GitHub

Other Article

Comments