【AWS SAM】iOS実機↔︎SAMの通信で "Bad file descriptor" エラーが出た時に見る記事

# はじめに

AWS SAMで1日半ハマり激萎え、そして解決。その備忘録です。
結構ピンポイントの問題に関する記事ですが、同じハマり方をする人は結構居そうだと思ったので投稿します。

# 対象読者
- **AWS SAM**（Serverless Application Model）を使ってAPIを開発している方
- XcodeでiOS (watchOS, visionOS) 開発をしている方
- 上記のiOSなどの実機から、ローカルのSAMのAPIにリクエストを送る設計のアプリをつくっt

# 環境
| ツール | バージョン |
| - | - |
| MacOS | `15.0.1` |
| Xcode | `16.1` |
| AWS SAM CLI（Runtime: Java21） | `1.135.0` |
| Python | `3.13` |
| Colima | `0.7.5` |

# 筆者のアプリの構造

認証などは端折って書いていますが、おおまかにこのような構造です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2633746/8d8ad9af-b0f0-437a-9e61-103a45e015bd.png)

ローカル開発は、このように実施します
- クライアント：iPhone実機
- API：AWS SAMでAPI GatewayとLambdaをローカル再現
- DB: Dynamodb-localのimageを使ってコンテナを立ち上げ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2633746/d0211cdb-0d10-41d8-aa3d-b8cacd132fc8.png)

# 起こった現象

状況を再現します。


### 1. SAMの起動

下記コマンドでSAMを起動しました
※docker containerで動いているdynamoDBに接続するために、docker netowrkを指定していますが、今回はあまり気にしなくていいです
```bash
sam local start-api --host 0.0.0.0 --docker-network dynamo-network
```

起動すると下記のようなログが出力されます
```bash
# ~~ 一部省略 ~~
Containers Initialization is done.

Mounting GetCurrentVersion at http://127.0.0.1:3000/api/version [GET, OPTIONS]

You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. If you used sam build before     
running local commands, you will need to re-run sam build for the changes to be picked up. You only need to restart SAM CLI if you update your AWS SAM template

 * * Tip: There are .env or .flaskenv files present. Do "pip install python-dotenv" to use them.
2025-04-06 14:41:24 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:3000
 * Running on http://xxx.xxx.xxx.xxx:3000

2025-04-06 14:41:24 Press CTRL+C to quit
```

`http://127.0.0.1:3000`、`http://xxx.xxx.xxx.xxx:3000`
でapiがホストされました
`/api/version`というGETのエンドポイントを一つ実装している前提です。

:::note info 
**SAMの起動で重要なところ**
```bash
--host 0.0.0.0
```
このオプションは、SAM CLI を使って iPhone 実機や他の端末からアクセスするために 必須です。
- SAM CLIは、デフォルトではlocalhost（＝127.0.0.1）だけにバインドされます
- つまり、Mac上のアプリやブラウザからしかアクセスできません
- 同じWi-Fiの iPhone 実機や別PCからのリクエストは届きません

これを忘れると、実機iPhoneからのリクエストは、SAMに一生届きません。

【ログの比較】
```bash
# ❌ --host オプションなしで起動（デフォルト）
sam local start-api

# localhost のみバインド
> * Running on http://127.0.0.1:3000
```

```bash
# ✅ --host 0.0.0.0 を指定して起動
sam local start-api --host 0.0.0.0

# 全てのインターフェースにバインドされる
> * Running on all addresses (0.0.0.0)
> * Running on http://127.0.0.1:3000
> * Running on http://xxx.xxx.xxx.xxx:3000 ← MacのローカルIPもOK！
```

:::

### 2. ブラウザからAPIを叩いてみる

`http://localhost:3000/api/version`
ブラウザから叩いてみると、正常にレスポンスが返ってきます✅
順調、順調。

### 3. iOS実機からAPIを叩いてみる

以下のような実装で、iOSからSAMにリクエストを送ります
かなり端折っていますが、要は該当API `/api/version` にGETリクエストを送っています。

```swift
let urlRequest = URLRequest(url: URL("http://xxx.xxx.xxx.xxx:3000/api/version")!)
try await URLSession.shared.data(for: urlRequest)
```
:::note info 
**iOSの実装で重要なところ**
urlを`http://localhost:3000`とせずに、
`http://xxx.xxx.xxx.xxx:3000`としているのは、
iPhone実機から`localhost`にリクエストを送ると、MacではなくiPhone自体のlocalhostを参照してしまうからです。

`xxx.xxx.xxx.xxx`に入る値は、
MacがWi-Fiルーターから取得したプライベートIPアドレスです。
iPhone実機などと同じWi-Fiに接続していれば、LAN内通信に使えるIPとなります。
下記コマンドで取得できます。

```bash 
ipconfig getifaddr en0

> xxx.xxx.xxx.xxx # MacのプライベートIP
```
:::

### 出力されたエラー

iOSからのリクエストが失敗しました。❌

#### iOS側

```bash
in handle error: Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost." UserInfo={_kCFStreamErrorCodeKey=-4, NSUnderlyingError=0x303535b60 {Error Domain=kCFErrorDomainCFNetwork Code=-1005 "(null)" UserInfo={NSErrorPeerAddressKey=<CFData 0x3018090e0 [0x1ffb73830]>{length = 16, capacity = 16, bytes = 0x10020bb8a9fef85f0000000000000000}, _kCFStreamErrorCodeKey=-4, _kCFStreamErrorDomainKey=4}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <171B3E0B-1287-405E-8255-25E16D8A3EF0>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <171B3E0B-1287-405E-8255-25E16D8A3EF0>.<1>"
), NSLocalizedDescription=The network connection was lost., NSErrorFailingURLStringKey=http://xxx.xxx.xxx.xxx:3000/api/version, NSErrorFailingURLKey=http://xxx.xxx.xxx.xxx:3000/api/version, _kCFStreamErrorDomainKey=4}
```


#### SAM側
```bash
----------------------------------------
Exception occurred during processing of request from ('xxx.xxx.xxx.xxx', 64319)
----------------------------------------
Exception occurred during processing of request from ('xxx.xxx.xxx.xxx', 64320)
----------------------------------------
Exception occurred during processing of request from ('xxx.xxx.xxx.xxx', 64321)
Traceback (most recent call last):
Traceback (most recent call last):
----------------------------------------
Exception occurred during processing of request from ('xxx.xxx.xxx.xxx', 64322)

Traceback (most recent call last):
  File "/opt/homebrew/Cellar/python@3.13/3.13.2/Frameworks/Python.framework/Versions/3.13/lib/python3.13/socketserver.py", line 697, in process_request_thread
    self.finish_request(request, client_address)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.2/Frameworks/Python.framework/Versions/3.13/lib/python3.13/socketserver.py", line 362, in finish_request
    self.RequestHandlerClass(request, client_address, self)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.2/Frameworks/Python.framework/Versions/3.13/lib/python3.13/socketserver.py", line 766, in __init__
    self.handle()
    ~~~~~~~~~~~^^

# ~~ 一部省略 ~~

Traceback (most recent call last):
  File "/opt/homebrew/Cellar/python@3.13/3.13.2/Frameworks/Python.framework/Versions/3.13/lib/python3.13/socketserver.py", line 362, in finish_request
    self.RequestHandlerClass(request, client_address, self)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
----------------------------------------
  File "/opt/homebrew/Cellar/python@3.13/3.13.2/Frameworks/Python.framework/Versions/3.13/lib/python3.13/socket.py", line 719, in readinto
    return self._sock.recv_into(b)
           ~~~~~~~~~~~~~~~~~~~~^^^
OSError: [Errno 9] Bad file descriptor

# ~~ 一部省略 ~~

OSError: [Errno 57] Socket is not connected
    

```

`OSError: [Errno 9] Bad file descriptor`　この部分が怪しい。
全体的に、SAMのAPIのPython製エミュレータ（Werkzeug + socketserver）がクラッシュしている感じです。

# 原因

結論、
SAM CLI v1.135.0（＝Python 3.13 + Werkzeug）に含まれる
「開発用HTTPサーバーの不具合」っぽいです。

つまりSAMの新しいバージョンでは、
「SAM CLIが内部で動かしている簡易Webサーバー」が調子悪いみたいです。

| SAM CLI ver | Python | Werkzeug | 状態 |
| - | - | - | - |
| ✅ v1.128.0 | 3.11 | 2.2.x〜2.3.x	| 安定 |
| ❌ v1.129.0〜v1.135.0 | 3.12〜3.13 | 2.3.x以降 | 不安定（Bad file descriptor） |

# やったこと

SAM CLI のバージョンを `1.135.0` → `1.128.0`に ダウングレードしました。
1日半あれこれハマりましたが、最終的にはそれだけで解消しました。それだけで。

# おわりに
こういう依存関係でハマるのが一番しんどいですね。

iOS × SAM で開発されている方はご注意を！