【Swift】Vapor で URLSession を使用したいときの注意点

こんにちは。[kamimi](https://twitter.com/kamimi_01)です。🌞

Vapor で API を実装中です。ようやく Google Cloud Run（以下GCR） にデプロイする段階にきて、イメージをビルドしたときにとあるエラーが発生しました。

ローカルでビルドしている時は気づかなかったので、今後の自分や他の人のために記事に残しておこうと思います。

# 発生したエラー

ようやく API を実装し終わったので、GCR にデプロイしようと思い、以下のコマンドを実行しました。

```shell
gcloud builds submit --tag gcr.io/iosappreviewstatuschecker/checker --timeout=20m
```

その時に以下のようなエラーが発生しました。
「この型は、FoundationNetworking モジュールに移動しました。使用するには、そのモジュールをインポートしてください。」

```shell
/build/Sources/App/Infrastructure/API/Session.swift:19:26: error: 'URLSession' is unavailable: This type has moved to the FoundationNetworking module. Import that module to use it.
    private let session: URLSession
                         ^~~~~~~~~~
```

# 対処方法

調べたところ、そもそも本当に`URLSession`を使う必要があるのか？を考えた方が良さそうです。理由は後ほど記述します。その上で使いたい場合は後述の処理をしましょう。

## 本当に `URLSession` を使う必要があるか？

私の場合、最終的に `URLSession` を使用しないという選択肢をとりました。機能を削ったわけではなく、別の方法で実装しました。

みなさんもこの代替方法が適用可能かを考えた上で、`URLSession`を使用するべきか考えてみてください。

結論、以下の理由から私は使用しませんでした。

1. そもそも `URLSession` を使用しなくても済むように、Vapor が API を提供しており、それで事足りた
1. Vapor では Linux のための Foundation の非同期 API が使用できない（[`data`メソッド](https://developer.apple.com/documentation/foundation/urlsession/3767352-data)などの基本的な API が使用できない）
1. デプロイ時のイメージサイズが大きくなる

### 理由1. そもそも `URLSession` を使用しなくても済むように、Vapor が API を提供しており、それで事足りた

私の場合、`URLSession`を使用したい理由は、外部の API を呼びたいからでした。後述する「[やっぱり `URLSession` を使いたい場合]()」の章に記載した通りに対処すれば、イメージビルド時にエラーは発生しなくなりました。

ただドキュメントを見ていたところ、`URLSession` を使用しなくても済むように、**Vapor が API を提供してくれている**ことがわかりました。

https://docs.vapor.codes/basics/client/

サーバーサイドの実装に疎いせいか、iOS アプリの実装と同じ感覚で実装していたせいか、 Vapor がそういった API を用意していることに気づかないまま、 いつもの感覚で `URLSession` を使用していました。

ちなみに以下のように使用することができます。（ドキュメントからそのまま引用）

```swift
let response = try await req.client.post("https://httpbin.org/status/200") { req in
    // Encode query string to the request URL.
    try req.query.encode(["q": "test"])

    // Encode JSON to the request body.
    try req.content.encode(["hello": "world"])

    // Add auth header to the request
    let auth = BasicAuthorization(username: "something", password: "somethingelse")
    req.headers.basicAuthorization = auth
}
// Handle the response.
```

例えば自分で API のレスポンスをもっとカスタマイズしたい場合などは、カスタマイズの仕方によってこの API は使用しないという判断になる可能性もあると思います。

カスタマイズ性については調べていないので詳しくはわかりませんが、**Vapor が便利な API を提供してくれているので、この API を使用した方がコードが複雑にならず可読性が上がって良いのでは**と思いました。

### 理由2. Vapor では Linux のための Foundation の非同期 API が使用できない

**Vapor では Linux のための Foundation の非同期 API が使用できません。** ローカルでビルドして localhost で動作確認しているときには問題なく動くので、気づかなかったです。。😇

実際、私は以下のように実装していたのですが、イメージのビルド時にエラーになってしまいました・・・

```swift
let (data, response) = try await URLSession.shared.data(for: urlRequest)
```

以下の stack overflow で Linux のための Foundation の非同期 API が使用できないことを知りました。回答者が Vapor Core Team のお一人である [0xTim さん](https://github.com/0xTim)なので、かなり信用度の高い情報だと思っています。

> The async APIs for Foundation on Linux aren't available yet (they're different from the actual language concurrency features).
For a Vapor app you shouldn't really be using URLSession to make requests, it doesn't fit with how the framework works. Use Vapor's client instead (which has async APIs)

https://stackoverflow.com/questions/70308688/deploying-a-vapor-app-to-heroku-fails-because-of-error-value-of-type-urlsess

回答にも、Vapor のクライアント API を代わりに使用してください、とありますね。
フレームワークの動作に合わないから、とのこと。


### 理由3. デプロイ時のイメージサイズが大きくなる

どうしても `URLSession` を使用したい場合、`libcurl4` というライブラリをインストールする必要があります。ですがそうするとイメージのサイズが大きくなってしまいます。

どのくらいサイズが大きくなるのかは、以下の Issue コメントで調べてくれた人がいました。🙏🏻

https://github.com/vapor/template/issues/86#issuecomment-1171781914

イメージが大きくなると、デプロイする時に時間がかかったり、ストレージを消費してしまったりするので、小さくできるならばした方が良いようです。

理由1 でも書いたように、Vapor が API を提供してくれているので、それを使えば良いかと思います。


## やっぱり `URLSession` を使いたい場合

それでも `URLSession` を使いたい場合、以下を実施すればビルドが成功するようになります。

1. `libcurl4` をインストール
1. `FoundationNetworking` をインポートする

### 手順1. `libcurl4` をインストール

Dockerfileの `RUN` にある`libcurl4`のコメントアウトを外します。
これでうまく動くようになります。簡単ですね。

```dockerfile:Dockerfile
# Make sure all system packages are up to date, and install only essential packages.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
    && apt-get -q update \
    && apt-get -q dist-upgrade -y \
    && apt-get -q install -y \
      ca-certificates \
      tzdata \
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
      libcurl4 \  # ここのコメントアウトを外す
# If your app or its dependencies import FoundationXML, also install `libxml2`.
      # libxml2 \
    && rm -r /var/lib/apt/lists/*
```

コメントに FoundationNetworking をインポートする場合は `libcurl4` をインストールしてね、と書いており親切だなと思いました。

この親切なコメントが入ったのは以下の Issue がきっかけのようです。感謝。

https://github.com/vapor/template/issues/86

### 手順2. `FoundationNetworking` をインポートする

次に `FoundationNetworking` をインポートする必要があります。

`URLSession` を使用するソースコードのファイルで、以下のように書きます。
Linux では `URLSession` は `FoundationNetworking` というモジュールに存在しているためです。

```swift
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
```

# おわりに

`URLSession` を使うのは避けるべきだということに気づいたのが、本番デプロイをするときになってしまったために、書き直すハメになってしまいました。。。が勉強にはなりましたし、この記事を書くきっかけになったので結果オーライだと思うことにしました！笑