简体   繁体   中英

PHP: headers already sent when using fwrite but not when using fputcsv

I know the theory behind this error however it is now driving me crazy again. I'm using Tonic in my application. With this library you redirect all traffic to your dispatch.php script which then executes the appropriate Resource and that Resource return a Response which is displayed (output) by dispatch.php .

The output method of Response look like this:

/**
 * Output the response
 */
public function output()
{
    foreach ($this->headers as $name => $value) {
        header($name.': '.$value, true, $this->responseCode());
    }
    echo $this->body;
}

So AFAIK this tells us that you can't write anything to php output in your Resource .

I now have a Resource that dynamically generates a csv from an input csv and outputs it to the browser (it converts 1 column of the data to a different format).

$csv = fopen('php://output', 'w');
// sets header
$response->__set('contentDisposition:', 'attachment; filename="' . $fileName . '"');
while (($line = fgetcsv($filePointer, 0, ",", '"')) !== FALSE) {
    // generate line
    fputcsv($csv, $line);
}
fclose($filePointer);
return $response;

This works 100% fine. No issue with headers and the correct file is generated and offered for download. This is already confusing because we are writing to the output before headers are set? What does fputcsv actually do?

I have a second resource that does a similar thing but it outputs a custom file format (text file).

$output = fopen('php://output', 'w');
// sets header
$response->__set('contentDisposition:', 'attachment; filename="' . $fileName . '"');
while (($line = fgetcsv($filePointer, 0, ",", '"')) !== FALSE) {
    // generate a record (multiple lines) not shown / snipped
    fwrite($output, $record);
}
fclose($filePointer);
return $response;

The only difference is that it uses fwrite instead of fputcsv and bang

 headers already sent by... // line number = fwrite()

This is very confusing! IMHO it should actually fail in both cases? Why does the first one work? how can I get the second one to work? (I can generate a huge string containing the file and put that into the responses body and it works. However files could be rather big (up to 50 mb) and hence want to avoid this.)

$record is not set, generating an error of level NOTICE . If you have error_reporting to true , PHP will put this error in the output before sending the headers.

Set error_reporting to false and keep an eye on your logs instead.

Here my solution. I'm not going to mark it as answer for a while because maybe someone comes up with something better (simpler) than this.

First a comment about fwrite and fputcsv:

fputcsv has a complete different source and not much in common with fwrite (it does not call fwrite internally, it's a separate function in C source code). Since I don't know CI can't tell why they behave differently but they do.

Solution:

The generated files can be "large" depending on input and hence generating the whole file by string concatenation and keeping it in memory isn't a great solution.

I googled a bit and found mod_xsendfile for apache. This works by setting a custom header in php containing the path to the file to be sent to the user. The mod then removes that custom header and sends the file as response.

The problem with mod_xsendfile is that it is not compatible with mod_rewrite, which I use too. You will get 404 errors. To solve this you need to add

RewriteCond %{REQUEST_FILENAME} !-f

to the according place in apache config (don't rewrite if the request is for an actual physically existing file). However that's not enough. You need to set the header X-Sendfile in a php script that was not rewritten, is an actual existing php file.

So in the \\Tonic\\Resource class generating the file at the end I redirect to an above outlined script:

$response->__set('location', $url . "?fileName=" . urlencode($fileName));
$response->code = \Tonic\Response::FOUND;
return $response;

In the download script we redirect to in above snippet just do (validation stuff omitted):

$filePath = trim($_GET['fileName']);
header ('X-Sendfile: ' . $filePath);    
header ('Content-Disposition: attachment; filename="' . $filePath . '"');

and the browser will display a download dialog for the generated file.

You will also need to create cron job to delete the generated files.

/usr/bin/find /path/to/generatedFiles/ -type f -mmin +60 -exec rm {} + 

this will delete all files older than 60 min in directory /path/to/generatedFiles .

I use ubuntu server so you can add it to the file

/etc/cron.daily/standard

or generated a new file in that directory or generated a new file in /etc/cron.hourly containing that command.

Note:

I name the generated files after the sha1 hash of the input csv file so the name is unique and if someone repeats the same requests several times in a short period you can just return the already generated file a second time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM