psr7-sessions / storageless

:mailbox_with_mail: storage-less PSR-7 session support
MIT License
649 stars 38 forks source link

Session Data not Persisting across Requests #585

Closed benplummer closed 10 months ago

benplummer commented 10 months ago

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.


declare(strict_types=1);

use FastRoute\Dispatcher;
use Laminas\Diactoros\ResponseFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Lcobucci\JWT\Configuration as JwtConfig;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\InMemory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;
use TestApp\ApplicationFactory;

require __DIR__ . '/../vendor/autoload.php';

$app = ApplicationFactory::create();

$dispatcher = FastRoute\simpleDispatcher(
    function(FastRoute\RouteCollector $r) {
        $r->addRoute('GET', '/counter', function(ServerRequestInterface $request) {

            $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);

            $session->set('counter', $session->get('counter', 0) + 1);

            $responseFactory = new ResponseFactory();

            $response = $responseFactory->createResponse();

            $response->getBody()
                ->write('Counter Value: ' . $session->get('counter'));

            return $response;
        });
    }
);

// Session Middleware
$app->addMiddleware(
    new SessionMiddleware(
        new StoragelessConfig(
            JwtConfig::forSymmetricSigner(
                new Signer\Hmac\Sha256(),
                InMemory::base64Encoded('zvLeE+12shOtGENokCcyrQ9HPeyk97YdOlTK2sGsq1o='),
            )
        )
    )
);

// Emitter Middleware
$app->addMiddleware(
    new class() implements MiddlewareInterface {

        public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
        {
            $response = $handler->handle(
                $request
            );

            (new SapiEmitter())->emit(
                $response,
            );

            return $response;
        }
    }
);

// Route Middleware
$app->addMiddleware(
    new class($dispatcher) implements MiddlewareInterface {

        public function __construct(private Dispatcher $dispatcher)
        {
        }

        public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
        {
            $httpMethod = $request->getMethod();
            $uri = (string) $request->getUri();

            if (false !== $pos = strpos($uri, '?')) {
                $uri = substr($uri, 0, $pos);
            }

            $uri = rawurldecode($uri);

            $routeInfo = $this->dispatcher->dispatch($httpMethod, $uri);

            $responseFactory = new ResponseFactory();

            $response = $responseFactory->createResponse()
                ->withHeader('Content-Type', 'text/html');

            switch ($routeInfo[0]) {
                case FastRoute\Dispatcher::NOT_FOUND:

                    return $response->withStatus(404);

                case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:

                    $allowedMethods = $routeInfo[1];
                    return $response->withStatus(405);

                case FastRoute\Dispatcher::FOUND:

                    $handler = $routeInfo[1];
                    return $handler($request);
            }
        }
    }
);

$app->run();

And this is the Application.php class that acts as the PSR-15 request handler.


declare(strict_types=1);

namespace TestApp;

use Laminas\Diactoros\ServerRequestFactory;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class Application implements RequestHandlerInterface
{
    private ?ContainerInterface $container = null;

    private array $middlewareStack = [];

    private ServerRequestFactoryInterface $serverRequestFactory;

    private function __construct(
        ServerRequestFactoryInterface $serverRequestFactory,
        ?ContainerInterface $container = null,
    )
    {
        $this->serverRequestFactory = $serverRequestFactory;
        $this->container = $container;
    }

    public function addMiddleware(MiddlewareInterface $middleware): static
    {
        $this->middlewareStack[] = $middleware;

        return $this;
    }

    public static function create(
        ?ContainerInterface $container = null,
        ?ServerRequestFactoryInterface $serverRequestFactory = null,
    ) : static
    {
        if($serverRequestFactory === null) {
            $serverRequestFactory = $container && $container->has(ServerRequestFactoryInterface::class)
                ? $container->get(ServerRequestFactoryInterface::class)
                : null;

            if($serverRequestFactory === null) {
                $serverRequestFactory = new ServerRequestFactory();
            }
        }

        return new static(
            $serverRequestFactory,
            $container,
        );
    }

    public static function createFromContainer(
        ContainerInterface $container,
    ) : static
    {
        $serverRequestFactory = $container->has(ServerRequestFactoryInterface::class)
            && (
                $serverRequestFactoryFromContainer = $container->get(ServerRequestFactoryInterface::class)
            ) instanceof ServerRequestFactoryInterface
            ? $serverRequestFactoryFromContainer
            : new ServerRequestFactory();

        return new static(
            $serverRequestFactory,
            $container,
        );
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        if(count($this->middlewareStack) === 0) {
            // TODO return proper 404 response
            die('404');
        }

        $nextMiddleware = array_shift(
            $this->middlewareStack,
        );

        return $nextMiddleware->process(
            $request,
            $this
        );
    }

    public function run(): ResponseInterface
    {
        $request = $this->serverRequestFactory->createServerRequest(
            $_SERVER['REQUEST_METHOD'],
            $_SERVER['REQUEST_URI'],
            $_SERVER,
        )->withCookieParams(
            $_COOKIE,
        )->withQueryParams(
            $_GET,
        )->withParsedBody(
            $_POST,
        );

        return $this->handle(
            $request,
        );
    }
}

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 the Application::run method above. The implementation I am using for the PSR-17 ServerRequestFactoryInterface is Laminas Diactoros. As far as I can tell, Diactoros does not marshal the headers from the $_SERVER params in createServerRequest() like it does in fromGlobals(). 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.

Ocramius commented 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 commented 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?

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"
      }
    ]
  }
}
Ocramius commented 10 months ago

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?)

benplummer commented 10 months ago

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
Ocramius commented 10 months ago

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:

benplummer commented 10 months ago

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)
Ocramius commented 10 months ago

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
Ocramius commented 10 months ago

Could you check what is happening in SessionMiddleware#appendToken()? 🤔

benplummer commented 10 months ago

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.

Ocramius commented 10 months ago

Oh, so something is replacing your response (or headers) after this middleware! 🤔

lcobucci commented 10 months ago

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 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!

benplummer commented 10 months ago

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 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!

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!

lcobucci commented 10 months ago

@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!