かきノート

【Laravel】ダウンロード処理の途中に、echo や var_dump 等の標準出力処理があった場合、正常に動かなくなる事がある2

July 21, 2022 • ☕️ 4 min read

【 環境 】
Laravel のバージョン: 8.61.1
PHP のバージョン: 8.0.16
MySQL のバージョン: 5.7.32

先日、こんなのを書きました。

ダウンロード処理の途中に、echo や var_dump 等の標準出力処理があった場合、正常に動かなくなる事がある

これについて、もう少し詳しく調べてみました。

おさらい:前回のソース

    public function fileDownloadAction01(Request $request)
    {
        $filePath = 'PdfFile01.pdf';
        $fileName = 'fileDownloadAction01' . '_' . Carbon::now('UTC')->format('YmdHisu');
        $headers = [
            'Content-Type' => 'application/pdf'
        ];

        // 何かしらのメッセージを表示する。↓のコードがあると、エラーとなる。
        echo "Some Message";

        return Storage::download($filePath, $fileName, $headers);
    }

ダウンロード処理の HTTP 仕様について

上記では Laravel のユーティリティを使用してダウンロード処理を記述しましたが、フレームワークを使わない場合、ダウンロード処理は、こんな感じになります。

    public function fileDownloadAction08($request)
    {
        $fileData = 'Hello World';

        header('Content-Disposition: attachment;');
        echo $fileData;
        exit;
    }

HTTP ヘッダに「Content-Disposition: attachment;」を付けて、データを標準出力で渡せば、それがダウンロード処理となるようです。

ヘッダの詳細については、こちらをご参照ください。

Content-Disposition - HTTP | MDN

実際は上記のように必要最低限のコードで実現する事は無く、以下のように別のヘッダも必要になるかと思われます。

    public function fileDownloadAction09($request)
    {
        $contentType = 'pdf';
        $fileData    = 'Hello World';
        $fileName    = 'MyDownloadFile09.pdf';

        header('Content-Type: ' . $contentType);                                  // Content-Type
        header('X-Content-Type-Options: nosniff');                                // ウェブブラウザが独自にMIMEタイプを判断する処理を抑止する
        header('Content-Length: ' . strlen((string)$fileData));                   // ダウンロードファイルのサイズ
        header('Content-Disposition: attachment; filename="' . $fileName . '"');  // ダウンロード時のファイル名  ※最低限、これがあればいい
        header('Connection: close');                                              // keep-aliveを無効にする
        echo $fileData;  // 出力
        exit;
    }

Laravel の Storage::download にて内部的に「Content-Disposition」が記述されているのであれば、echo や var_dump を記述しているとエラーが発生するのも納得です。

という訳で、Laravel のコードを調べてみます。

Laravel のコードを調査

framework\src\Illuminate\Support\Facades\Storage.php

namespace Illuminate\Support\Facades;

use Illuminate\Filesystem\Filesystem;

/**
 * @method static \Illuminate\Contracts\Filesystem\Filesystem assertExists(string|array $path)
 * @method static \Illuminate\Contracts\Filesystem\Filesystem assertMissing(string|array $path)
 *
 * //(中略)
 *
 * @see \Illuminate\Filesystem\FilesystemManager
 */

//以下略

framework\src\Illuminate\Filesystem\FilesystemAdapter.php

namespace Illuminate\Filesystem;

use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystemContract;
use Illuminate\Contracts\Filesystem\FileExistsException as ContractFileExistsException;
use Illuminate\Contracts\Filesystem\FileNotFoundException as ContractFileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as FilesystemContract;

//(中略)

    /**
     * Create a streamed download response for a given file.
     *
     * @param  string  $path
     * @param  string|null  $name
     * @param  array|null  $headers
     * @return \Symfony\Component\HttpFoundation\StreamedResponse
     */
    public function download($path, $name = null, array $headers = [])
    {
        return $this->response($path, $name, $headers, 'attachment');
    }

//(中略)

    /**
     * Create a streamed response for a given file.
     *
     * @param  string  $path
     * @param  string|null  $name
     * @param  array|null  $headers
     * @param  string|null  $disposition
     * @return \Symfony\Component\HttpFoundation\StreamedResponse
     */
    public function response($path, $name = null, array $headers = [], $disposition = 'inline')
    {
        $response = new StreamedResponse;

        $filename = $name ?? basename($path);

        $disposition = $response->headers->makeDisposition(
            $disposition, $filename, $this->fallbackName($filename)
        );

        $response->headers->replace($headers + [
            'Content-Type' => $this->mimeType($path),
            'Content-Length' => $this->size($path),
            'Content-Disposition' => $disposition,
        ]);

        $response->setCallback(function () use ($path) {
            $stream = $this->readStream($path);
            fpassthru($stream);
            fclose($stream);
        });

        return $response;
    }

という事で、「Content-Disposition」が指定されている事を確認しました。

引数となっている変数 $disposition のデフォルト値は ‘inline’ ですが、download メソッドからコールされる場合は、その値は ‘attachment’ で固定されているので、上記の推測は間違っていなかったようです。

結論

ダウンロード処理を記述する場合、標準出力の処理を記述すると、HTTP 上の仕様の問題により、エラーとなる。

どんなフレームワークやライブラリを使用しても、内部的には HTTP の仕様を踏襲する以外は無いんじゃないかと思われるので、フレームワークやライブラリを問わずにこの現象が発生するものと思われます。


Relative Posts:

【Laravel】Observer には、どんな種類のメソッドが? ソースから解析してみた

September 27, 2022

【Laravel】ダウンロード処理の途中に、echo や var_dump 等の標準出力処理があった場合、正常に動かなくなる事がある

July 19, 2022

福岡の物流エンジニアが、七転び八起きしたあと九回転び、寝っ転がったまま何かやってる事を垂れ流しているブログ

RotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-icon