Retrofit+OkHttpでmultipart/form-dataなAPIの呼び出してハマった

### はじめに

multipart/form-dataなAPIを呼び出す実装が一番面倒だと思っているのですが、自前でゴリゴリと実装している人が意外と多い印象です。
Retrofit+OkHttpを使ったらどれぐらい簡単なのか試してみたのですがハマり所もあったので記事していきます。

### ハマりどころ

`@retrofit2.http.Multipart` `@retrofit2.http.Part` `@retrofit2.http.Body` `okhttp3.MultipartBody` `okhttp3.MultipartBody.Part` `okhttp3.RequestBody` あたりを組み合わせて実装するのですが、まずい組み合わせでハマったり、必要なリクエストヘッダーが指定できそうで出来なかったりと使い方が分かるまで結構大変でした。

https://stackoverflow.com/questions/34562950/post-multipart-form-data-using-retrofit-2-0-including-image

StackOverFlowでも反応が多いのでハマっている人の多さが伺い知れます。

### まずやっておいた方が良いこと

どんなリクエストデータが作成されたのか見れないとデバッグもままならないのでリクエストボディをログに出すようにInterceptorを仕込んでおくことをお勧めします。

```java
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .build();
```

これによって、生のリクエスト内容がログに出るので何が起きているか把握できるようになります。
ちなみにmultipart/form-dataのバウンダリーや改行がどう入るのかを見れるので勉強にもなるかと思います。

```
2021-12-04 22:43:08.014  INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient                     : --> POST http://localhost:8080/upload3
2021-12-04 22:43:08.014  INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient                     : Content-Type: multipart/form-data; boundary=1f2d4b84-5503-43b1-aa79-2e46bd64074f
2021-12-04 22:43:08.014  INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient                     : Content-Length: 343
2021-12-04 22:43:08.015  INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient                     : 
2021-12-04 22:43:08.015  INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient                     : --1f2d4b84-5503-43b1-aa79-2e46bd64074f
Content-Disposition: form-data; name="json"
Content-Length: 14

{"param": "1"}
--1f2d4b84-5503-43b1-aa79-2e46bd64074f
Content-Disposition: form-data; name="file"; filename="test.csv"
Content-Type: text/csv
Content-Length: 24

no,name
1,taro
2,hanako

--1f2d4b84-5503-43b1-aa79-2e46bd64074f--

2021-12-04 22:43:08.015  INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient                     : --> END POST (343-byte body)
```

## 実装方法その1

MutliPartBodyオブジェクトを自分で組み立てて`@Body`で渡すやり方。
後で出てくる`@Multipart`と`@Body`は組み合わせられないのですが、最初はそれをやろうとしてハマりました。

```java
interface UploadService {
    @POST("upload")
    Call<Map<String, String>> upload(@Body MultipartBody multipartBody);
}
```

次に呼び出し方。
`MutliPartBody.Builder.addFormDataPart`を使ってマルチパートのボディ部分を作るのが一番良さそうでした。
それに気づくまでは`addPart`を使ったり試行錯誤したのですが、思った通りのリクエスト内容にならなくて結構ハマりました。

```java
MultipartBody multipartBody = new MultipartBody.Builder()
    .setType(MultipartBody.FORM) //デフォルトはmultipart/mixedなのでmultipart/form-dataに設定し直す
    // Content-Typeを指定したくて試行錯誤した結果がこちら
    // filenameにnullを渡さないといけないのが気持ち悪いけど他に丁度いいインターフェイスがない
    .addFormDataPart("json", null,
        RequestBody.create(MediaType.get("application/json"), "{\"param\": \"1\"}"))
    .addFormDataPart("file", "test.csv",
        RequestBody.create(MediaType.get("text/csv"), new File("/tmp/test.csv")))
    .build();
service.upload(multipartBody).execute();
```

## 実装方法その2

今度は`@Multipart`を使うパターンです。
前述した通り`@Body`とは組み合わせられないので`@Part`を使います。

```java
interface UploadService2 {
    @Multipart
    @POST("upload3")
    Call<Map<String, String>> upload(@Part MultipartBody.Part json, @Part MultipartBody.Part file);
}
```
MultipartBody.Partを作ってAPIに渡します。作り方はその1と同じような感じでOKです。

```java
MultipartBody.Part json = MultipartBody.Part.createFormData("json", null,
    RequestBody.create(MediaType.get("application/json"), "{\"param\": \"1\"}"));
MultipartBody.Part file = MultipartBody.Part.createFormData("file", "test.csv",
    RequestBody.create(MediaType.get("text/csv"), new File("/tmp/test.csv")));
UploadService2 service2 = retrofit.create(UploadService2.class);
service2.upload(json, file).execute();
```

### おわり

以上、やりたいことが出来ました。
結局、かなり試行錯誤しないと実装出来ませんでしたが、やり方が分かってしまえばスッキリ短く書けました！