Why doesn't server-side script stop when EventSource is closed?
Asked Answered
E

2

8

TL;DR update: Whether closing an EventSource from the client or closing the client entirely, php continues to execute on the backend and fails to report the correct value for connection_aborted(). Why might this be?

I have been all over Google and Stack Overflow looking for answers to this, but none of the suggested fixes resolve the issue: I have a simple page testing the functionality of server-sent events using JavaScript and php. Everything was working well until I realized that server script execution did not stop when a client navigated to another page or refreshed itself. This appears to be a common problem and the suggestions given in other questions have borne no fruit for me.

StackOverflow questions I've already investigated

Linked articles & other material I've already investigated

I have stripped the code of everything that I thought could be a possible culprit and I'm still having the issue. I am especially surprised that connection_aborted continues to report false after the explicit EventSource.close() call in the client or simply closing the client before the 10 second server loop has finished. Here is my exact code, after stripping out everything but the server-sent event stuff:

sse_tests.js

document.addEventListener('DOMContentLoaded', () => {
  // Set up EventSource for receiving server-sent events.
  const testEventSource = new EventSource('sse_tests.php');
  testEventSource.addEventListener('test', (e) => {
    const data = JSON.parse(e.data);
    console.log(`count: ${data.count}`);
    if (data.count >= 5) {
      testEventSource.close();
    }
  });
});

sse_tests.php

<?php
// Set the event stream header(s).
header("Cache-Control: no-cache");
header("Content-Type: text/event-stream");

// XXX Override automatic detection of client abortion; we'll do it ourselves.
// (This was suggested in another answer, and I have the same issue with or without it)
ignore_user_abort(true);

// Initialize an arbitrary count parameter to investigate client communication.
$count = 1;

while ($count <= 10) {
  $eventData = json_encode(array(
    "count" => $count,
  ));

  echo "event: test\n";
  echo "data: ${eventData}";
  echo "\n\n";

  ob_flush();
  flush();

  $aborted = connection_aborted();

  error_log("count: ${count}, connection_aborted: ${aborted}");

  if ($aborted) {
    break;
  }

  $count++;

  sleep(1);
}

The client successfully opens the connection, tracks it for 5 emissions of the test event and then stops seeing any further emissions of the test event, but the server continues to execute for the full count of 10, even after the testEventSource.close() call or a closing of the browser window before the full count of 10, as evidenced by the server log contents here:

count: 1, connection_aborted: 0
count: 2, connection_aborted: 0
count: 3, connection_aborted: 0
count: 4, connection_aborted: 0
count: 5, connection_aborted: 0
count: 6, connection_aborted: 0
count: 7, connection_aborted: 0
count: 8, connection_aborted: 0
count: 9, connection_aborted: 0
count: 10, connection_aborted: 0

I'm on shared hosting with php 7.2 and minimal tweaks to server configuration. Let me know if that could be the source of strife, and I'll try to investigate more of the default configurations and share whatever else is needed.

Exteriorize answered 17/4, 2020 at 14:29 Comment(15)
What is the value of connection_status()?Warwickshire
@Warwickshire The status is 0 (NORMAL)Exteriorize
in your php.ini do you have these two lines? output_buffering = 0 implicit_flush = 1Warwickshire
@miknik, I don't have a php.ini file, and I've just been relying on the defaults provided in the shared hosting package. The hosting admin panel is exposed through CPanel, if you're familiar with that; do you know where I could find those settings? No big deal, if not. Should those lines have those values? Or would that potentially be the cause of the problem if those lines had those values?Exteriorize
What if you add ` ob_end_flush(); ob_implicit_flush();` to the beginning of your script?Warwickshire
@miknik, same issue, except every line in the server log is now interleaved with the message: PHP Notice: ob_flush(): failed to flush buffer. No buffer to flush in /home/brijtpxm/dev_includes/cody/controllers/sse_tests.php on line 25. Line 25 is the call to ob_flushExteriorize
Sounds like you have some output buffering going on somewhere. I just copied your code to my server and it works perfectly.Warwickshire
@miknik, it stops executing on the server after the count of 5 instead of going to 10?Exteriorize
Correct, and console log on the web page updates with each count at the expected interval.Warwickshire
@Warwickshire Yikes -.- Do you know anything about shared hosting and where I could look for some wonky defaults that might be throwing my specific setup off? I've mostly punted on all of that and gotten by just fine until this point. Literally any insight would be helpful; I wouldn't know whether to dig into php configuration or apache or something else.Exteriorize
Nope sorry, Im guessing they all differ and there is a good possibility that you don't have control over all the settings. Personally I'd ditch it, you can get VM hosting for $5/month and this may be the first time you have run into an annoying issue, but it won't be the last...Warwickshire
@Warwickshire That's kind of what I was thinking. Do you have suggestions for any services that balance ease of use with sensible defaults and complete control over configuration if you need it? I've been eyeballing GCP with either App Engine or Compute Engine, but it seems like getting up to speed with all of that would set me back several weeks.Exteriorize
Let us continue this discussion in chat.Warwickshire
Short answer as @Warwickshire it's related to the buffer. PHP doesn't check if the user aborted until it try to send something back. Check my full answer here: #5997602Adversity
Does this answer your question? PHP Server Sent Events Connection won't close?Ensemble
M
2

Based on the updated information, I believe this is definitely wrong:

ignore_user_abort(true);

This should be:

ignore_user_abort(false);

... so that the disconnect is not ignored. What's happening after that is something of a mystery. MDN reports that the connection is persistent, so it could be a bug in the browser that does not close the connection. It could be a problem with a load balancer or reverse proxy, if those are present. It could be a problem in the web server. Or it could be a problem in the PHP configuration.

In the chat it looks like the test server was most likely nginx with PHP running under FastCGI. Are you using Apache with mod_php instead? If so, does it still happen when you run PHP as a CGI? That should help isolate the issue further.

I would also use connection_status() just so you can be more certain about the connection state. https://www.php.net/manual/en/features.connection-handling.php

Missionary answered 27/4, 2020 at 3:23 Comment(0)
W
2

Well. The problem is likely happening because you are using an intermediate server (e.g., proxy). So when you send a close event from the client i.e., testEventSource.close();, the connection between the client and the proxy must be closed and the problem you are likely to have is the proxy is keeping the connection with the server, hence, that's why you have the connection open from the server side.

Wane answered 17/6, 2023 at 3:1 Comment(1)
i faced something similiar when using remote vscode. its port forwarding feature was the culprit.Bhopal

© 2022 - 2024 — McMap. All rights reserved.