Troubleshooting
The SSE route returns 404
Check package discovery:
composer dump-autoload
php spark routes
Confirm:
- Composer package discovery is enabled in
Config\Modules; route['enabled']istrue;routedoes not conflict with an earlier application route;- the request URL includes the application's configured base path.
If the route was customized, use that path in the browser client.
The endpoint returns 406
The default Redis configuration requires this header for a direct PHP stream request:
Accept: text/event-stream
Native browser EventSource sends it. Add the header when testing with curl:
curl -N \
-H 'Accept: text/event-stream' \
'http://localhost:8080/sse?channels=public.test'
Do not disable requireAcceptHeader merely to compensate for an incorrectly
configured proxy that strips request headers.
The endpoint returns 400
The channels query parameter is missing or invalid.
Valid requests include:
/sse?channels=public.news
/sse?channels=users.42,orders.918
/sse?channels[]=users.42&channels[]=orders.918
Check the channel syntax and maxChannelsPerConnection. Oversized raw
channels query input is rejected before splitting. Patterns are rejected
unless explicitly enabled.
The endpoint returns 403
There are two common causes:
- one or more channels failed
ChannelAuthorizerInterface; - a cross-origin request is not in
allowedOrigins.
Verify that UserResolverInterface::resolve() returns the expected
authenticated object. Do not “fix” a private-channel denial by switching to an
authorizer that permits every channel.
Redis health check fails
Run:
php spark sse:health-check
Then verify:
- Redis
scheme,host, andport; - container or firewall networking;
- ACL username and password;
- selected Redis database;
- CA path and peer name for TLS;
- the PHP process has permission to open outbound stream sockets.
The package does not use PhpRedis, so installing that extension does not fix a TCP, TLS, ACL, or application configuration problem.
Custom broker is not loaded
The broker entry in Sse::$brokers must resolve to BrokerAdapterInterface.
Use exactly one of:
factory: aBrokerAdapterFactoryInterfaceinstance, class name, or callable returning one;adapter: aBrokerAdapterInterfaceinstance, class name, or callable returning one.
If both keys are present, remove one so the resolver has a single construction path.
If the error says the factory or adapter class does not exist, verify the namespace and Composer autoload, then run:
composer dump-autoload
If the error says the configured broker does not provide a PHP subscriber,
either implement SubscriberAwareBrokerAdapterInterface plus
SubscriberInterface, or return a custom SubscriptionEndpointInterface that
does not need the PHP stream manager.
See Custom brokers for the exact interfaces and minimal implementation.
Mercure publish fails
The Debug Toolbar and thrown MercurePublishException include the Hub status.
Typical causes are:
401: publisher key or JWT does not match the Hub;403:publisherTopicSelectorsdoes not include the mapped topic;400: invalid Hub form field or an outdated Hub;- connection failure:
hubUrlis not reachable from the PHP runtime; - TLS failure: CA bundle or hostname verification is incorrect.
In Docker, PHP normally publishes to a service URL such as:
http://mercure/.well-known/mercure
The browser must receive a separate publicHubUrl, such as:
https://app.example.com/.well-known/mercure
Confirm that the Hub publisher key equals sse.mercure.publisherKey. Use
Mercure 0.24.2 or newer in the 0.x line.
Mercure authorization succeeds but EventSource returns 401
Verify:
- the Hub subscriber key equals
sse.mercure.subscriberKey; - Hub
cookie_namematchesmercure.cookie.name; - the cookie path includes
/.well-known/mercure; Secureis disabled only for local plain HTTP;- cookie domain covers both the CodeIgniter response host and Hub host;
withCredentialsis enabled;- Hub CORS lists the exact application origin.
Inspect the authorization request in browser developer tools. The JSON
topics array must contain the expected topics and the response must set the
subscriber cookie.
The cookie is HttpOnly, so it will not appear through document.cookie.
Browser client reports adapter-error
The selected frontend adapter could not resolve the EventSource URL. For Mercure, inspect the short CodeIgniter authorization request:
400means the channel list is invalid;403means channel policy or application CORS denied the request;5xxmeans the active broker could not build its authorization response;- an invalid JSON shape means a proxy or custom controller replaced the package response.
This error occurs before EventSource connects to the external Hub.
The connection opens but no events arrive
Check:
- publisher and endpoint use the same Redis configuration;
- both use the same
channelPrefix; - the logical publish channel exactly matches the requested channel;
- the event is published after the subscriber connects;
- proxy buffering is disabled;
- a worker or queue process has reloaded current environment configuration.
Redis Pub/Sub does not replay events published before the subscriber was connected.
The configured sse.connected event proves the HTTP response opened; it does
not prove a later application publish used the same Redis instance.
Named handler does not run
The listener name must equal the event name:
live.on('order.updated', handler);
An SSE frame with event: order.updated does not invoke the browser's default
onmessage callback. Register each named event used by the page. The wrapper's
global message handler sees named events that the wrapper is already observing.
JSON payload is not parsed
Inspect:
live.on('message', ({ parsed, raw, parseError }) => {
console.log({ parsed, raw, parseError });
});
The client intentionally keeps the connection alive when one payload is
invalid. Correct the publisher or custom serializer; do not use eval() or
insert untrusted raw strings as HTML.
Events arrive in a burst
An intermediate layer is buffering. Verify:
- Nginx
proxy_buffering offorfastcgi_buffering off; - gzip disabled for the SSE route;
- CDN caching and transformation disabled;
X-Accel-Buffering: nopreserved;- the PHP runtime flushes output;
- test command uses
curl -N.
Browser reconnects every few minutes
This is expected when maxConnectionSeconds is finite. The lifecycle normally
looks like:
open → reconnecting → open
The server sends a retry hint and native EventSource reconnects. Investigate
only if reconnect never returns to open, occurs much faster than configured,
or creates duplicate clients in application code.
Normal requests freeze while SSE is open
A locking PHP session may still be held by custom authentication code. Resolve the authenticated user before starting the stream and release the session lock when no more session writes are required.
Also check PHP-FPM saturation: each SSE connection normally occupies a worker.
Many users receive 502/503 responses
Inspect:
- PHP-FPM
pm.max_children; - application worker memory;
- proxy upstream connection limits;
- Redis subscriber count;
- reconnect bursts after deploys;
- number of EventSource instances per page.
Prefer one stream with multiple authorized channels. For high concurrency, move long-lived connections to a dedicated SSE gateway while CI4 continues to publish through Redis.
Cross-origin cookies are missing
All of these must agree:
- browser client
withCredentials: true; - exact origin in
allowedOrigins; - server
withCredentials = true; - HTTPS;
- cookie
Domain,Secure, andSameSiteattributes.
Access-Control-Allow-Origin: * cannot be used with credentialed requests.
Standard EventSource cannot replace the missing cookie with an arbitrary
authorization header.
If the browser console reports a Content Security Policy violation, add the
SSE origin to the application's connect-src directive. CORS and CSP are
independent checks; both must permit a cross-origin stream.