Skip to content

headers_sent(&$filename, &$linenum) returns true and yet doesn't set either filename or linenum #2523

Open
@php4fan

Description

@php4fan

From manual page: https://php.net/function.headers-sent


It says:

If the optional filename and line parameters are set, headers_sent() will put the PHP source file name and line number where output started in the filename and line variables.

Therefore, if I call headers_sent($filename, $linenum) and pass it both arguments, and it returns true, I expect it to set both filename and linenum to the file and line number where the output started.

And yet, I (pretty often) observe cases where it returns true and yet doesn't set filename and line number. I don't know how to reproduce, because, because of the very fact that it doesn't tell me where the output started, I have been unable to debug it. Otherwise I would be happy to provide example code that reproduces.

If there are legitimate cases for headers_sent() being unable to tell the file name and line number where the output started, then these cases should be documented in excruciating detail.

But I can't think of any such legitimate cases. My script is initiated by a php file, so wherever the output started, that "somewhere" MUST be at least backtraceable to some filename and line number (if it was started by eval()'d code, then it's the filename and line number that called eval(), that is given that you decided that only a filename and line number are to be returned and not a backtrace of any sort).

Activity

php4fan

php4fan commented on Jun 5, 2023

@php4fan
ContributorAuthor

Oh cr** I followed the "report a bug" link in the documentation, but I intended to report this against PHP as an actual bug.

transferred this issue fromphp/doc-enon Jun 5, 2023
damianwadley

damianwadley commented on Jun 5, 2023

@damianwadley
Member

I could imagine some built-in function emitting a warning message being one possible cause for headers being outputted without a concrete file and line number. Not saying that it should do that, but it's a potential explanation.

Do you get any "cannot send headers, output started at..." error messages? Either they'll have a file and line number themselves or, perhaps more likely, they'll cite Unknown:0.

KapitanOczywisty

KapitanOczywisty commented on Jun 5, 2023

@KapitanOczywisty
Contributor

@php4fan You can try to use ob_start to find where output was started. This will obviously add significant overhead to any output, so before sending intended data use ob_end_clean.

Example code: https://3v4l.org/kMt6f

<?php

// example logging function
function debug($msg){
    // send data to browser without using output buffering
    fwrite(STDOUT, sprintf("[Debug] %s\n", $msg));
}

if(headers_sent($file, $line)){
    // if output started before calling ob_start
    debug("Headers already sent at {$file}:{$line}");
} else {
    // catch any unexpected output using ob_start(chunk_size:1)
    ob_start(function($buffer, $phase) {
        // ignore ob_end_*() call
        if(($phase & PHP_OUTPUT_HANDLER_FINAL) === 0) {
            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
            foreach($backtrace as $frame){
                // ob_start callback can sometimes have backtrace frames without file/line
                if(isset($frame['file'], $frame['line'])){
                    debug("Unexpected output at: {$frame['file']}:{$frame['line']}");
                    break;
                }
            }
        }
        // send output to browser, return NULL to suppress output
        return $buffer;
    }, 1);
}

// some unexpeced output: undefined variable warning
$a++;
// unexpected echo
echo "Oops\n";

// before sending intended output remove output buffering
// otherwise there will be significant overhead for every output
ob_end_clean();

echo "Normal output\n";

Edit: You can also use header_register_callback https://3v4l.org/vm8KO

<?php

header_register_callback(function(){
    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
    foreach($backtrace as $frame){
        if(isset($frame['file'], $frame['line'])){
            echo "Output started: {$frame['file']}:{$frame['line']}\n";
            break;
        }
    }
});

echo "data";
php4fan

php4fan commented on Jun 5, 2023

@php4fan
ContributorAuthor

I found out what actually starts the output and triggers the issue in my case.

It's the warning about the number of uploaded files exceeding max_file_uploads. That is emitted before script execution has even started, and therefore the concepts of file name and line number don't apply at all.

The very idea of headers_sent() "returning" a file name and line number is flawed, because the concepts of file name and line number don't always apply, and don't always satisfactorily answer the question "When/where did output start?".

Note, by the way, that even when the concepts of filename and line number do apply, they can sometimes be of very little help in debugging: if the file and line belong to a function that is called from many different places in the code, what one really would want to see is a backtrace (and in the case of situations like mine, where output is started outside of/before actual php code, extra information).

The only decent solution would be to provide a better API that gives all the information. For exmple, instead of just "returning" a filename and line number, "return" an array:

headers_sent(&$info) // $info being an array

Obviously there's BC to take into account. So either this:

headers_sent(&$filename, &$linenum, &$info) // $info being an array

or

headers_sent(&$filename_or_array, &$linenum);

but this last one would rely on $filename_or_array having to be already set to an array, and only in this case it would be filled as such; I don't know which is worse: this one or having 3 arguments. Anyway, all of this would be just for BC.

The point is, "file name and line number" is not enough to identify the place where output started, so it's an ill-designed interface.

Shorter-term solution

In the meantime, while sticking to the "filename + line number" interface (given that I guess that will take years to overcome, if it ever is), I think it would be very important to return special values in $filename, rather than the empty string, for distinguishable cases. At the very, very least something signifying "Before script execution" or "Startup" or something, because right now it requires quite a leap of faith to assume that that is the case (hopefully it's the only case in which filename can currently be empty, but even you didn't immediately assume that it was. If that is really true, then it should be made 100% sure and then stated in the docs). But ideally something more than that.

(note: even this requires some thought regarding BC, as there's bound to be code out there checking whether $filename is empty, and if it is not, assuming it is a file name).

KapitanOczywisty

KapitanOczywisty commented on Jun 5, 2023

@KapitanOczywisty
Contributor

It's the warning about the number of uploaded files exceeding max_file_uploads. That is emitted before script execution has even started, and therefore the concepts of file name and line number don't apply at all.

In general php errors should not be sent to browser, but If sent e.g. on debug enviroment, you should see that upload warning before headers sent one. Even if you have logger set up (and for some reason same errors are also shown in browser), you likely will see upload warning and then warning about headers sent right after. So real solution is to improve logging situation.

php4fan

php4fan commented on Jun 5, 2023

@php4fan
ContributorAuthor

In general php errors should not be sent to browser, but If sent e.g. on debug enviroment, you should see that upload warning before headers sent one.

(emphasis added) Except I don't see it because I am not the user.

So real solution is to improve logging situation.

I am very aware that my "logging situation" is not ideal; I may have my reasons for that or I could just be being sloppy. Either way, PHP's job is to... do its job. headers_sent() is supposed to tell me where the output has started, whatever the conditions are. Given that display_errors exists and can be on (regardless of whether it's good practice or not to have it turned on), the output starting because of a warning is something that can happen and therefore should be taken into account.

KapitanOczywisty

KapitanOczywisty commented on Jun 6, 2023

@KapitanOczywisty
Contributor

Showing errors to users is a security issue, and fixing that should be the first priority.

Going back to proposition: What exactly is improved by having headers_sent return some "startup" condition instead of empty string? I don't think empty string can indicate any other condition than startup, so rather than returning unexpected not-a-filename in $filename variable or changing function signature, I'd improve documentation by mentioning that $filename and $line will be empty when output was started before executing PHP file.

header_sent returning backtrace could be useful in very rare occasions, but I'm not sure this justifies generating backtrace every single time output is started, which would be needed. And I don't think people have header_sent check before normal output, so "headers already sent" warning would need to be also changed?

Nevertheless, if you think this should be implemented anyway, you can go through RFC process.

DeveloperRob

DeveloperRob commented on Jun 6, 2023

@DeveloperRob
Contributor

@KapitanOczywisty: If the headers are sent by something other than PHP, will headers_sent() return true? I am not sure what kind of setup would lead to this, but if this is possible; it could be a legitimate reason to set $filename to php_startup if PHP generated the output prior to script execution (and potentially pre_php if the output occurred beforehand).

php4fan

php4fan commented on Jun 6, 2023

@php4fan
ContributorAuthor

I don't think empty string can indicate any other condition than startup, so rather than returning unexpected not-a-filename in $filename variable or changing function signature, I'd improve documentation by mentioning that $filename and $line will be empty when output was started before executing PHP file.

By all means, if you are 100% sure that that is the only case, then do that. Otherwise, document all the other cases too.

KapitanOczywisty

KapitanOczywisty commented on Jun 6, 2023

@KapitanOczywisty
Contributor

[...] I'd improve documentation by mentioning that $filename and $line will be empty when output was started before executing PHP file.

By all means, if you are 100% sure that that is the only case, then do that.

I'm not 100% sure, but also until you or anybody else finds some other case, we won't be able to return that in headers_sent anyway, so for now this is true-enough statement.

If the headers are sent by something other than PHP

Output has to go through PHP output handler to be detected and headers to be sent, If something (not sure what this could be) sends data before that, you will likely get 500 error from apache/nginx. We're talking about something in PHP causing output before main PHP script is executed - e.g. handing upload, parsing input (_POST, _GET) etc.

php4fan

php4fan commented on Jun 6, 2023

@php4fan
ContributorAuthor

I'm not 100% sure, but also until you or anybody else finds some other case, we won't be able to return that in headers_sent anyway, so for now this is true-enough statement.

Ok then I guess that statement should go into the documentation. Then if anyone ever finds a counter-example, they/we can report it as a documentation issue.

transferred this issue fromphp/php-srcon Jun 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @damianwadley@php4fan@KapitanOczywisty@DeveloperRob

        Issue actions

          headers_sent(&$filename, &$linenum) returns true and yet doesn't set either filename or linenum · Issue #2523 · php/doc-en