Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WiP: Non buffered body #2504

Merged
merged 3 commits into from
Apr 26, 2019
Merged

WiP: Non buffered body #2504

merged 3 commits into from
Apr 26, 2019

Conversation

1ma
Copy link

@1ma 1ma commented Sep 19, 2018

This PR implements the proposal made in #2481 as a new subtype of StreamInterface.

Code

The implementation of most methods is trivial because the interface was designed for classes wrapping an internal stream, and in this case there simply isn't one.

write($string) essentially destroys all active Output Buffering levels (but saving their contents), then prepends this to $string and flushes everything to the webserver. Successive calls to NonBufferedBody::write() won't destroy anything else (unless the programmer runs ob_start() and echo in-between calls for any reason) and flush $string right away.

Any previously buffered data is displayed in a FIFO order. In other words, data echoed at the output buffer level 1 will be printed before data echoed at level 2.

Additionally, I had to modify Response::withHeader($name, $value) to send the headers eagerly when the body is of type NonBufferedBody, otherwise the framework won't attempt to send the headers until the end stages of App::run(). This change is quite delicate but I believe it cannot break any previous code because NonBufferedBody simply did not exist before.

Working example

<?php

use Slim\Http;

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

$app = new \Slim\App;

$app->get('/', function (Http\Request $request, Http\Response $response) {
    $response = $response
        ->withHeader('Content-Type', 'text/plain');

    foreach (range('A', 'F') as $char) {
        sleep(1);
        $response->getBody()->write("$char\n");
    }
});

$app->add(function (Http\Request $request, Http\Response $response, callable $next) {
    $nonBufferedResponse = $response
        ->withBody(new Http\NonBufferedBody)
        ->withHeader('X-Accel-Buffering', 'no');

    return $next($request, $nonBufferedResponse);
});

$app->run();

Gotchas

HTTP headers must be sent before the content

When pushing data to the webserver while still running PHP code, one must send the headers first. Calling Response::withHeader($name, $value) after any call to NonBufferedBody::write($string) will trigger an Cannot modify header information - headers already sent PHP error. Obviously, after sending the first chunks of data you cannot push more headers in the middle of the body.

NonBufferedBody must be set on the response before setting any HTTP header

Since Response now looks at the type of the $body attribute to determine whether to send HTTP headers eagerly or not, it is imperative to replace the underlying StreamInterface of the response with Response::withBody(new NonBufferedBody) before doing any call to Response::withHeader($name, $value).

I believe that this issue is best avoided with a "non-buffered middleware" that can be attached to the routes that need to behave this way, like in the example.

Some middlewares may break

As noted by @llvdl in the original issue, outgoing middlewares won't be able to modify the body of unbuffered responses nor add any HTTP headers. Only appending new data to the body will be possible in this scenario, which is generally not terribly useful.

Webservers buffer output by default

Even after calling PHP's flush() function webservers usually buffer the output on their own.

Nginx + php-fpm

Nginx has the fastcgi_buffering off; directive to explicitly disable output buffering. Additionally, you can send a special X-Accel-Buffering header set to no to do it dynamically from PHP (see example).

Apache

??? TBD

TODO

  • Tests
  • Documentation
  • Better class naming?

@coveralls
Copy link

coveralls commented Sep 19, 2018

Coverage Status

Coverage decreased (-0.6%) to 97.186% when pulling a16312e on 1ma:non-buffered-body into 04e7689 on slimphp:3.x.

@akrabat
Copy link
Member

akrabat commented Nov 25, 2018

Is it worth doing this in Slim-Psr7 for use with Slim 4?

@1ma
Copy link
Author

1ma commented Nov 25, 2018

I believe it is, yes.

@akrabat
Copy link
Member

akrabat commented Dec 8, 2018

Looks good to me. We need a docs PR for it on Slim-Website.

@l0gicgate l0gicgate added this to the 3.13.0 milestone Feb 18, 2019
@l0gicgate
Copy link
Member

Where are you at with this @1ma do you need more feedback?

Just a couple things from my end:

  • Test coverage is incomplete for NonBufferedBody
  • I can do the docs PR if you you can summarize the functionality in detail
  • Class naming is adequate in my opinion

Thanks!

@1ma
Copy link
Author

1ma commented Mar 3, 2019

My problem is that I don't see a satisfactory way to test the NonBufferedBody::write() method, as it has an echo.

For this kind of thing I'd usually wrap the system under test in a ob_start()/ob_get_clean() pair and make assertions on the returned string, but this write() method needs precisely to end any active output buffers, so the echoed data always goes straight to STDOUT. Do you have any ideas?

@l0gicgate l0gicgate removed this from the 3.13.0 milestone Apr 16, 2019
@l0gicgate l0gicgate added this to the 3.12.2 milestone Apr 23, 2019
@l0gicgate
Copy link
Member

I understand @1ma. After some thought I think this PR is fine, it's an opt-in use with some gotchas described above. Could you make a PR on the Slim-Website repo documenting the feature? I'll merge after. Thanks

@1ma
Copy link
Author

1ma commented Apr 23, 2019

Great, will do.

@l0gicgate l0gicgate merged commit 833fea9 into slimphp:3.x Apr 26, 2019
@l0gicgate
Copy link
Member

Let's port this to Slim-Psr7 as well @1ma. I'll try and do the docs for it actually this weekend, I'll have you review them.

@1ma
Copy link
Author

1ma commented Apr 27, 2019

Works for me. I don't intend to shy away from the work, but writing in English is not really my forte :) Thank you.

@1ma 1ma deleted the non-buffered-body branch April 27, 2019 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants