Skip to content

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'] is true;
  • route does 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, and port;
  • 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: a BrokerAdapterFactoryInterface instance, class name, or callable returning one;
  • adapter: a BrokerAdapterInterface instance, 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: publisherTopicSelectors does not include the mapped topic;
  • 400: invalid Hub form field or an outdated Hub;
  • connection failure: hubUrl is 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_name matches mercure.cookie.name;
  • the cookie path includes /.well-known/mercure;
  • Secure is disabled only for local plain HTTP;
  • cookie domain covers both the CodeIgniter response host and Hub host;
  • withCredentials is 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:

  • 400 means the channel list is invalid;
  • 403 means channel policy or application CORS denied the request;
  • 5xx means 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:

  1. publisher and endpoint use the same Redis configuration;
  2. both use the same channelPrefix;
  3. the logical publish channel exactly matches the requested channel;
  4. the event is published after the subscriber connects;
  5. proxy buffering is disabled;
  6. 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 off or fastcgi_buffering off;
  • gzip disabled for the SSE route;
  • CDN caching and transformation disabled;
  • X-Accel-Buffering: no preserved;
  • the PHP runtime flushes output;
  • test command uses curl -N.

See Streaming and deployment.

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, and SameSite attributes.

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.