デザインワン・ジャパン Tech Blog

DesignOne Japan | Activate the World.

【Guzzle】アップロード(multipart/form-dataを送信)したファイルが壊れていた際の対応

はじめに

こんにちは!株式会社デザインワン・ジャパンで口コミサービス エキテンのリニューアルを担当しているサービス開発部の鈴木セシル(@suzuki_cecil_)です。

弊社では現在、15年間運用され続けている口コミサービス エキテン のリニューアルプロジェクトに取り組んでおります。

リニューアルプロジェクトではHTTPクライアントに Guzzle を利用しているのですが、今回、Guzzleによるファイルのアップロードで少々苦戦することとなったためその時の内容を記事にまとめたいと思います。

Guzzleによるファイルアップロード方法1(Request Optionsにmultipartを指定)

POSTやPUTでファイルをアップロードするためには、Content-Typemultipart/form-dataにする必要があります。GuzzleでContent-Typemultipart/form-dataにする方法としては一般に、以下のサンプルコード1のようにRequest Optionsmultipartを指定する方法が知られているかなと思います。

サンプルコード1

<?php

use GuzzleHttp\Client;

class FileStorage
{
    private Client $client;

    public function __construct(private Client $client)
    {
    }

    public function upload(UploadedFile $uploadedFile)
    {
        $this->client->put("https://example.jp/test_1.jpeg",
            [
                'multipart' => [
                    [
                        'name' => '',
                        'contents' => fopen($uploadedFile->openFile()->getRealPath(), "r"),
                    ],
                ]
            ]
        );
    }
}

GuzzleではRequest Optionsをコンストラクタインジェクションもしくはメソッドインジェクションすることで柔軟にリクエストをカスタマイズすることが可能です。

サンプルコード1ではRequest Optionsmultipartを指定することで、multipart/form-dataを送信するようにカスタマイズしてあります。

ファイルアップロード処理の実装が完了したと思い、サンプルコード1を実行して動作確認したところ、ファイルのアップロード先に test_1.jpeg が生成されていたのですが、生成されたファイルが壊れていました。

ファイルが壊れているということで、悪い予感がしたため test_1.jpeg のバイナリデータを確認したところ、やはりヘッダ情報がバイナリデータに含まれていました。

  Content-Disposition: form-data; name="file"; filename="phpMnWyGV"^M
  Content-Length: 32643^M 

そのため test_1.jpeg がjpgのフォーマットに則っておらずファイルが壊れていると認識されていたのです。

通常であれば方法1として記述した、Request Optionsにmultipartを指定する方法でファイルのアップロードを行うことは可能であるはずなのですが、今回私が試したところ何故かファイルのバイナリデータにヘッダ情報が含まれてしまいました(今回、ファイルが壊れた理由は判明できていないです。)

そこで後述するRequest Optionsにheadersを指定する方法でmultipart/form-dataを送信し、ファイルのアップロードを試みてみました。

Guzzleによるファイルアップロード方法2(Request Optionsにheadersを指定)

Request Optionsmultipartを指定する以外の方法で、multipart/form-dataを送信できないかを確認するために公式ドキュメントのRequest Optionsのページを確認したところ headersの項目を見つけました。

公式ドキュメントによるとRequest Optionsmultipartを指定するのではなくheadersとしてリクエストヘッダを直接定義することが可能なようでした。Request Optionsheadersを指定するようにしたのがサンプルコード2です。

サンプルコード2

<?php

use GuzzleHttp\Client;

class FileStorage
{
    private Client $client;

    public function __construct(private Client $client)
    {
    }

    public function upload(UploadedFile $uploadedFile)
    {
        $this->client->put("https://example.jp/test_2.jpeg",
            [
                'body' => fopen($uploadedFile->openFile()->getRealPath(), "r"),
                'headers' => ['Content-Type' => 'multipart/form-data'],
            ]
        );
    }
}

サンプルコード2を実行してみたところファイルのアップロード先に test_2.jpeg が作られている、またバイナリデータにheaderが含まれていないことが確認できました。

まとめ

公式ドキュメントのRequest Optionsのページのmultipartには以下のように記載があります。

Summary

Sets the body of the request to a multipart/form-data form.

リクエストのボディを multipart/form-data 形式で設定します、とあるためサンプルコード1とサンプルコード2は同義だと思うのですが、サンプルコード1ではファイルのバイナリデータにヘッダ情報が含まれ、サンプルコード2ではファイルのバイナリデータにヘッダ情報が含まれないという結果となりました。

今回のケースでは方法1として記述したRequest Optionsにmultipartを指定する方法だとファイルのバイナリデータにヘッダ情報が含まれてしまったため、方法2として記述したRequest Optionsにheadersを指定する方法で、リクエストのボディを multipart/form-data 形式で設定する方が安全であると考えます。

仲間を募集しております

募集中の職種については以下を御覧ください。

www.wantedly.com