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);
}
上記では 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 のコードを調べてみます。
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
*/
//以下略
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 の仕様を踏襲する以外は無いんじゃないかと思われるので、フレームワークやライブラリを問わずにこの現象が発生するものと思われます。