Remove duplicate "Set-Cookie" header from PHP response
Asked Answered
A

2

6

This is an example script from a larger application, but shows the general process of what I'm trying to do. If I have the following script:

<?php
ob_start();

setcookie('test1', 'first');
setcookie('test1', 'second');
setcookie('test1', 'third');
setcookie('test2', 'keep');

//TODO remove duplicate test1 from headers

ob_end_clean();
die('end test');

I get the following response (as viewed via Fiddler):

HTTP/1.1 200 OK
Date: Tue, 25 Apr 2017 21:54:45 GMT
Server: Apache/2.4.17 (Win32) OpenSSL/1.0.2d PHP/5.5.30
X-Powered-By: PHP/5.5.30
Set-Cookie: test1=first
Set-Cookie: test1=second
Set-Cookie: test1=third
Set-Cookie: test2=keep
Content-Length: 8
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

end test

The problem is that Set-Cookie: test1... exists 3 different times, therefore increasing the header size unnecessarily. (Again, this is a simplified example - in reality, I'm dealing with ~10 duplicate cookies in the ~800-byte range.)

Is there anything I can write in place of the TODO that would get rid of the header either completely or so it only shows once? i.e. the following is my end goal:

HTTP/1.1 200 OK
Date: Tue, 25 Apr 2017 21:54:45 GMT
Server: Apache/2.4.17 (Win32) OpenSSL/1.0.2d PHP/5.5.30
X-Powered-By: PHP/5.5.30
Set-Cookie: test1=third
Set-Cookie: test2=keep
Content-Length: 8
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

end test

though the Set-Cookie: test1=third could not exist too and that's fine, but Set-Cookie: test2=keep needs to remain. When I try setcookie('test1', '', 1); to delete the cookie, it adds an additional header to mark it as expired:

Set-Cookie: test1=first
Set-Cookie: test1=second
Set-Cookie: test1=third
Set-Cookie: test2=keep
Set-Cookie: test1=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0

And if I try removing the header like:

if (!headers_sent()) {
    foreach (headers_list() as $header) {
        if (stripos($header, 'Set-Cookie: test1') !== false) {
            header_remove('Set-Cookie');
        }
    }
}

it removes all Set-Cookie headers when I only want test1 removed.

Astrobiology answered 25/4, 2017 at 22:10 Comment(2)
Could you store the headers in an array? e.g. if you had a View class, that could have a (static?) method SetCookie() which would store the key and value in an associative array. Then when sending the final headers, just iterate over that associative array and call setcookie(), which would only get called once per key...Vinasse
I wish! Ideally that's how this would be set up, but by the time it gets to my script the Set-Cookie: test1=... has already been run multiple times, so the place I can inject any sort of code is at the TODOAstrobiology
D
5

As you suggested in that last block of code, the headers_list() function could be used to check what headers have been sent. Using that, the last values for each cookie could be stored in an associative array. The names and values can be extracted using explode() (along with trim()).

When multiple cookies with the same name have been detected, we can use the header_remove() call like you had, but then set the cookies to the final values. See the example below, as well as this example phpfiddle.

if (!headers_sent()) {
    $cookiesSet = []; //associative array to store the last value for each cookie
    $rectifyCookies = false; //multiple values detected for same cookie name

    foreach (headers_list() as $header) {
        if (stripos($header, 'Set-Cookie:') !== false) {
            list($setCookie, $cookieValue) = explode(':', $header);
            list($cookieName, $cookieValue) = explode('=', trim($cookieValue));
            if (array_key_exists($cookieName, $cookiesSet)) {
                $rectifyCookies = true;
            }
            $cookiesSet[$cookieName] = $cookieValue;
        }
    }
    if ($rectifyCookies) {
        header_remove('Set-Cookie');
        foreach($cookiesSet as $cookieName => $cookieValue) {
            //might need to consider optional 3rd - 8th parameters 
            setcookie($cookieName, $cookieValue);
        }
    }
}

Output:

Cache-Control max-age=0, no-cache, no-store, must-revalidate
Connection keep-alive
Content-Encoding gzip
Content-Type text/html; charset=utf-8
Date Wed, 26 Apr 2017 15:31:33 GMT
Expires Wed, 11 Jan 1984 05:00:00 GMT
Pragma no-cache
Server nginx
Set-Cookie test1=third
                     test2=keep
Transfer-Encoding chunked
Vary Accept-Encoding

Dearden answered 26/4, 2017 at 15:44 Comment(4)
Awesome, this is exactly what I was looking for!Astrobiology
This is excellent. I would recommend changing two things to properly handle additional cookie header params. 1) change both explodes to limit at 2 Example: explode(':', $header, 2) 2) Use the header() function rather than setcookie() like header("Set-Cookie: {$cookieName}={$cookieValue}", false); These changes will maintain any expires, Max-Age, etc params from the original Set-Cookie headers.Dyandyana
@Dyandyana okay I am fine with adding the third parameter to the call to explode() but after reading this answer don't see much of a reason not to use setcookie() (unless the 32-bit 2038 overflow issue affects you)....Vinasse
Note this code could be put into a function and registered with header-register_callback()Wohlert
T
0

I don't understand why you think that the cookie removing code you showed us would remove the setcookie for test2.

If your code is setting the same cookie multiple times then you need to change your code so it stops setting the cookie multiple times! Anything else is a sloppy workaround.

Transatlantic answered 25/4, 2017 at 22:41 Comment(2)
You mean header_remove('Set-Cookie');? The way it's defined (php.net/manual/en/function.header-remove.php), it removes all Set-Cookie headers. So regardless of the cookie name, it clears them all. I'm working with a framework that seems to be adding the cookie multiple times, so I'm just trying to clean that up before the response is returned without hacking the core.Astrobiology
Doh, of course! Leaving aside the fact its the wrong way to fix the problem, you could just parse the setcookie headers into an array of name=>value, then header_remove('Set-Cookie') then iterate through your array again. But this then leads to a slightly less bad solution of just populating the array in the first place and not calling setcookie() until the data is marshalled (but its still the wrong solution)Transatlantic

© 2022 - 2024 — McMap. All rights reserved.