Closed benplummer closed 10 months ago
@benplummer since this is a large code dump, it's a bit hard to attack the problem from the code-side.
Are you able to record a few HTTP payloads and observe the cookies sent from the server and the client over multiple hops?
@benplummer since this is a large code dump, it's a bit hard to attack the problem from the code-side.
Are you able to record a few HTTP payloads and observe the cookies sent from the server and the client over multiple hops?
Sure, are Firefox Network logs okay?
Here are 3 calls to the counter
route.
Also, I forgot to mention in the original issue that I have configured https locally so I am accessing the app at https://localhost/counter
. Not sure if that affects anything but just in case.
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "121.0.1"
},
"browser": {
"name": "Firefox",
"version": "121.0.1"
},
"pages": [
{
"id": "page_1",
"pageTimings": {
"onContentLoad": 115,
"onLoad": 119
},
"startedDateTime": "2024-01-15T16:54:41.060+00:00",
"title": "https://localhost/counter"
}
],
"entries": [
{
"startedDateTime": "2024-01-15T16:54:41.060+00:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://localhost/counter",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Host",
"value": "localhost"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0"
},
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
},
{
"name": "Accept-Language",
"value": "en-GB,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "Sec-Fetch-Dest",
"value": "document"
},
{
"name": "Sec-Fetch-Mode",
"value": "navigate"
},
{
"name": "Sec-Fetch-Site",
"value": "none"
},
{
"name": "Sec-Fetch-User",
"value": "?1"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Sec-GPC",
"value": "1"
}
],
"cookies": [],
"queryString": [],
"headersSize": 472
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Server",
"value": "nginx/1.23.1"
},
{
"name": "Date",
"value": "Mon, 15 Jan 2024 16:54:41 GMT"
},
{
"name": "Content-Type",
"value": "text/html; charset=UTF-8"
},
{
"name": "Transfer-Encoding",
"value": "chunked"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "X-Powered-By",
"value": "PHP/8.2.12"
}
],
"cookies": [],
"content": {
"mimeType": "text/html; charset=UTF-8",
"size": 16,
"text": "Counter Value: 1"
},
"redirectURL": "",
"headersSize": 196,
"bodySize": 212
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 90,
"receive": 0
},
"time": 90,
"_securityState": "secure",
"serverIPAddress": "127.0.0.1",
"connection": "443",
"pageref": "page_1"
},
{
"startedDateTime": "2024-01-15T16:54:44.145+00:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://localhost/counter",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Host",
"value": "localhost"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0"
},
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
},
{
"name": "Accept-Language",
"value": "en-GB,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "Sec-Fetch-Dest",
"value": "document"
},
{
"name": "Sec-Fetch-Mode",
"value": "navigate"
},
{
"name": "Sec-Fetch-Site",
"value": "none"
},
{
"name": "Sec-Fetch-User",
"value": "?1"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Sec-GPC",
"value": "1"
}
],
"cookies": [],
"queryString": [],
"headersSize": 472
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Server",
"value": "nginx/1.23.1"
},
{
"name": "Date",
"value": "Mon, 15 Jan 2024 16:54:44 GMT"
},
{
"name": "Content-Type",
"value": "text/html; charset=UTF-8"
},
{
"name": "Transfer-Encoding",
"value": "chunked"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "X-Powered-By",
"value": "PHP/8.2.12"
}
],
"cookies": [],
"content": {
"mimeType": "text/html; charset=UTF-8",
"size": 16,
"text": "Counter Value: 1"
},
"redirectURL": "",
"headersSize": 196,
"bodySize": 212
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 77,
"receive": 0
},
"time": 77,
"_securityState": "secure",
"serverIPAddress": "127.0.0.1",
"connection": "443",
"pageref": "page_1"
},
{
"startedDateTime": "2024-01-15T16:54:46.210+00:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://localhost/counter",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Host",
"value": "localhost"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0"
},
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
},
{
"name": "Accept-Language",
"value": "en-GB,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "Sec-Fetch-Dest",
"value": "document"
},
{
"name": "Sec-Fetch-Mode",
"value": "navigate"
},
{
"name": "Sec-Fetch-Site",
"value": "cross-site"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Sec-GPC",
"value": "1"
}
],
"cookies": [],
"queryString": [],
"headersSize": 458
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Server",
"value": "nginx/1.23.1"
},
{
"name": "Date",
"value": "Mon, 15 Jan 2024 16:54:46 GMT"
},
{
"name": "Content-Type",
"value": "text/html; charset=UTF-8"
},
{
"name": "Transfer-Encoding",
"value": "chunked"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "X-Powered-By",
"value": "PHP/8.2.12"
}
],
"cookies": [],
"content": {
"mimeType": "text/html; charset=UTF-8",
"size": 16,
"text": "Counter Value: 1"
},
"redirectURL": "",
"headersSize": 196,
"bodySize": 212
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 87,
"receive": 0
},
"time": 87,
"_securityState": "secure",
"serverIPAddress": "127.0.0.1",
"connection": "443",
"pageref": "page_1"
}
]
}
}
From your response, it seems like no cookies are returned at all.
Could you try checking the raw response strings? I'm not sure if Firefox is filtering content here (this looks like a .har
format?)
From your response, it seems like no cookies are returned at all.
Could you try checking the raw response strings? I'm not sure if Firefox is filtering content here (this looks like a
.har
format?)
Sorry, I've copied the raw responses here. There are no cookies according to the tab in Firefox.
GET /counter HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
DNT: 1
Sec-GPC: 1
HTTP/1.1 200 OK
Server: nginx/1.23.1
Date: Mon, 15 Jan 2024 16:54:41 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.2.12
Counter Value: 1
GET /counter HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
DNT: 1
Sec-GPC: 1
HTTP/1.1 200 OK
Server: nginx/1.23.1
Date: Mon, 15 Jan 2024 16:54:44 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.2.12
Counter Value: 1
GET /counter HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
DNT: 1
Sec-GPC: 1
HTTP/1.1 200 OK
Server: nginx/1.23.1
Date: Mon, 15 Jan 2024 16:54:46 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.2.12
Counter Value: 1
Would my php.ini session settings affect any of this?
session.cache_expire = 30
session.cookie_httponly = 1
session.cookie_lifetime = 14400
session.cookie_samesite = Strict
session.cookie_secure = 1
session.name = EXAMPLESESSID
session.save_path = /var/www/html/sessions/
session.sid_bits_per_character = 6
session.sid_length = 256
session.use_cookies = 1
session.use_only_cookies = 1
session.use_strict_mode = 1
Next thing I'd check is if at the end of the execution of the SapiEmitter
$request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE)
contains valuable information, and dump the response object in there :thinking:
Next thing I'd check is if at the end of the execution of the
SapiEmitter
$request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE)
contains valuable information, and dump the response object in there 🤔
Removed the majority of the phrases
on the Response to condense it down but left the majority there as I'm uncertain what I should be looking for.
/var/www/html/public/index.php:204:
object(PSR7Sessions\Storageless\Session\LazySession)[33]
private ?PSR7Sessions\Storageless\Session\SessionInterface 'realSession' =>
object(PSR7Sessions\Storageless\Session\DefaultSessionData)[38]
private array 'data' =>
array (size=1)
'counter' => int 1
private array 'originalData' =>
array (size=0)
empty
private 'sessionLoader' =>
object(Closure)[32]
virtual 'closure' '$this->PSR7Sessions\Storageless\Http\{closure}'
public 'static' =>
array (size=1)
'token' => null
public 'this' =>
object(PSR7Sessions\Storageless\Http\SessionMiddleware)[6]
private readonly PSR7Sessions\Storageless\Http\Configuration 'config' =>
object(PSR7Sessions\Storageless\Http\Configuration)[8]
private Lcobucci\JWT\Configuration 'jwtConfiguration' =>
object(Lcobucci\JWT\Configuration)[21]
private Lcobucci\JWT\Parser 'parser' =>
object(Lcobucci\JWT\Token\Parser)[18]
private readonly Lcobucci\JWT\Decoder 'decoder' =>
object(Lcobucci\JWT\Encoding\JoseEncoder)[16]
private Lcobucci\JWT\Validator 'validator' =>
object(Lcobucci\JWT\Validation\Validator)[19]
private Closure 'builderFactory' =>
object(Closure)[20]
virtual 'closure' 'Lcobucci\JWT\Configuration::Lcobucci\JWT\{closure}'
public 'static' =>
array (size=1)
'encoder' =>
object(Lcobucci\JWT\Encoding\JoseEncoder)[15]
public 'parameter' =>
array (size=1)
'$claimFormatter' => string '<required>' (length=10)
private array 'validationConstraints' =>
array (size=0)
empty
private readonly Lcobucci\JWT\Signer 'signer' =>
object(Lcobucci\JWT\Signer\Hmac\Sha256)[7]
private readonly Lcobucci\JWT\Signer\Key 'signingKey' =>
object(Lcobucci\JWT\Signer\Key\InMemory)[5]
public readonly string 'contents' => string '���v��Ch�'2�G=줶:T�����Z' (length=32)
public readonly string 'passphrase' => string '' (length=0)
private readonly Lcobucci\JWT\Signer\Key 'verificationKey' =>
object(Lcobucci\JWT\Signer\Key\InMemory)[5]
public readonly string 'contents' => string '���v��Ch�'2�G=줶:T�����Z' (length=32)
public readonly string 'passphrase' => string '' (length=0)
private Lcobucci\Clock\Clock 'clock' =>
object(Lcobucci\Clock\SystemClock)[22]
private readonly DateTimeZone 'timezone' =>
object(DateTimeZone)[23]
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
private Dflydev\FigCookies\SetCookie 'cookie' =>
object(Dflydev\FigCookies\SetCookie)[24]
private 'name' => string '__Secure-slsession' (length=18)
private 'value' => null
private 'expires' => int 0
private 'maxAge' => int 0
private 'path' => string '/' (length=1)
private 'domain' => null
private 'secure' => boolean true
private 'httpOnly' => boolean true
private 'sameSite' =>
object(Dflydev\FigCookies\Modifier\SameSite)[25]
private 'value' => string 'Lax' (length=3)
private int 'idleTimeout' => int 43200
private int 'refreshTime' => int 60
private string 'sessionAttribute' => string 'session' (length=7)
private PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration 'clientFingerprintConfiguration' =>
object(PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration)[26]
private readonly array 'sources' =>
array (size=0)
empty
/var/www/html/public/index.php:204:
object(Laminas\Diactoros\Response)[42]
private array 'phrases' =>
array (size=66)
100 => string 'Continue' (length=8)
...
private string 'reasonPhrase' => string 'OK' (length=2)
private int 'statusCode' => int 200
protected 'headers' =>
array (size=0)
empty
protected 'headerNames' =>
array (size=0)
empty
private 'protocol' => string '1.1' (length=3)
private 'stream' =>
object(Laminas\Diactoros\Stream)[41]
protected 'resource' => resource(90, stream)
protected 'stream' => string 'php://memory' (length=12)
I see:
private array 'data' =>
array (size=1)
'counter' => int 1
private array 'originalData' =>
array (size=0)
empty
The data is therefore written, but I think the middleware is not appending it to the response object:
protected 'headers' =>
array (size=0)
empty
protected 'headerNames' =>
array (size=0)
empty
Could you check what is happening in SessionMiddleware#appendToken()
? 🤔
Could you check what is happening in
SessionMiddleware#appendToken()
? 🤔
It reaches here as the session container has changed but is not empty.
SessionMiddleware#appendToken()
and consequently SessionMiddleware#process()
returns the following:
/var/www/html/vendor/psr7-sessions/storageless/src/Storageless/Http/SessionMiddleware.php:74:
object(Laminas\Diactoros\Response)[37]
private array 'phrases' =>
array (size=66)
100 => string 'Continue' (length=8)
...
private string 'reasonPhrase' => string 'OK' (length=2)
private int 'statusCode' => int 200
protected 'headers' =>
array (size=1)
'Set-Cookie' =>
array (size=1)
0 => string '__Secure-slsession=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MDUzNDUzNDYsIm5iZiI6MTcwNTM0NTM0NiwiZXhwIjoxNzA1Mzg4NTQ2LCJzZXNzaW9uLWRhdGEiOnsiY291bnRlciI6MX19.8syQyVc1tbLCRlfTuPoH_YzcLYyRZqSFqxr5ZRe3Nj4; Path=/; Expires=Tue, 16 Jan 2024 07:02:26 GMT; Secure; HttpOnly; SameSite=Lax' (length=287)
protected 'headerNames' =>
array (size=1)
'set-cookie' => string 'Set-Cookie' (length=10)
private 'protocol' => string '1.1' (length=3)
private 'stream' =>
object(Laminas\Diactoros\Stream)[41]
protected 'resource' => resource(90, stream)
protected 'stream' => string 'php://memory' (length=12)
It appears the set-cookie
header is part of the response at that point.
Oh, so something is replacing your response (or headers) after this middleware! 🤔
Looking at the first comment here, I'd say it's related to the order of the middleware. Your flow is like this:
In other PSR-15 "applications", no middleware is responsible for emitting the response as we MUST guarantee that we'll always emit the "final result".
~I'd highly recommend using a well-tested solution maintained by the community (Mezzio is my preferred, honestly, followed by SlimPHP).
If you still prefer to create your own, then~ My recommendation would be to have a method in the Application
class that's responsible for calling Application#run()
and emitting the resulting response (the Emitter middleware would also have to be removed, sure).
Edit: I've missed the point where you're saying that your goal is not to use a framework, apologies!
Looking at the first comment here, I'd say it's related to the order of the middleware. Your flow is like this:
1. Init session (Session Middleware) 2. Call next middleware (Emitter Middleware) 3. Find route, execute handler, and return base response (Router Middleware) 4. Render response (Emitter Middleware) <-- No modification to the response is relevant after this point 5. Append cookie to the response (Session Middleware) - useless as the response has already been emitted
In other PSR-15 "applications", no middleware is responsible for emitting the response as we MUST guarantee that we'll always emit the "final result".
~I'd highly recommend using a well-tested solution maintained by the community (Mezzio is my preferred, honestly, followed by SlimPHP). If you still prefer to create your own, then~ My recommendation would be to have a method in the
Application
class that's responsible for callingApplication#run()
and emitting the resulting response (the Emitter middleware would also have to be removed, sure).Edit: I've missed the point where you're saying that your goal is not to use a framework, apologies!
You're right, thanks! I was in the middle of debugging the request in each middleware, and then I saw your comment, which has saved me some time!
I have been looking at some of the micro-frameworks as inspiration, particularly how they comply with the PSRs. I think with learning so many new concepts and looking at so many code samples in a short time, I ended up missing that emitting the response isn't usually implemented in middleware. All working now!
Thanks again @lcobucci, and thanks for all of your help too @Ocramius. Much appreciated!
@benplummer glad to hear it worked out!
Doing things ourselves does allow us to learn about several things we often overlook.
It's sometimes much harder, though 😅 Kudos for not giving up and for investing on your growth!
Hi, I'm fairly certain that this is not a bug or issue with your package, and that it is most likely an issue with how my code is using the package but hopefully someone will be able to shed some light on where things are going wrong.
I've been building a little app using PSR standard compliant components without using a framework. I've included the small snippet of counter example code as the only route but when I refresh, the counter value does not increment.
Here is the
index.php
.And this is the
Application.php
class that acts as the PSR-15 request handler.When I
var_dump
the$request
none of the expected headers exist. I wondered if this is because of the way I am creating$request
in theApplication::run
method above. The implementation I am using for the PSR-17ServerRequestFactoryInterface
is Laminas Diactoros. As far as I can tell, Diactoros does not marshal the headers from the $_SERVER params increateServerRequest()
like it does infromGlobals()
. Is this why the session is not persisting for me? Or is it a different reason?Apologies if it's something basic related to PSRs; I've only recently started looking at the later ones. Any help would be appreciated.