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
[HttpFoundation] Add support for the 103 status code (Early Hints) and other 1XX statuses #48128
base: 6.3
Are you sure you want to change the base?
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
aa3684a
to
85bb5ca
Compare
Hey! I think @pascalwoerde has recently worked with this code. Maybe they can help review this? Cheers! Carsonbot |
As many Symfony devs don't know about the
|
Is that deprecation necessary? Calling |
@chalasr What would be the benefit of doing that? My proposal is very similar to how it's implemented in Go for instance. IMHO the current API is good enough and there is no need to make it more complex. @derrabus the plan is to force passing |
And why don't we make it optional right from the start? |
It's not obvious that |
Informational responses are special: they can only be composed of headers, by the spec:
https://httpwg.org/specs/rfc9110.html#status.1xx So basically, sending 1XX responses mean sending headers ahead of time. Adding a new method would just add more confusion IMHO. |
Yes, that's something I personally know but:
But maybe it's just me
A virtual parameter ( |
@chalasr I hope that I've found an acceptable compromise:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\WebLink\Link;
class HomepageController extends AbstractController
{
#[Route("/", name: "homepage")]
public function index(Request $request): Response
{
$response = $this->sendEarlyHints([
(new Link(href: '/style.css'))->withAttribute('as', 'stylesheet'),
(new Link(href: '/script.js'))->withAttribute('as', 'script'),
]);
// Do something slow...
return $this->render('homepage/index.html.twig', response: $response);
}
} So we have the best of both worlds. WDYT? |
Looks great! Thanks |
Co-authored-by: Christophe Coevoet <stof@notk.org>
Co-authored-by: Robin Chalas <chalasr@users.noreply.github.com>
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
This sends the headers immediately to the client, right? How can I test a controller that makes use of that feature? |
Indeed. You can test using FrakenPHP and |
It would be nice to make sure that functional testing utilities are ready for this, and to patch them otherwise. Maybe even setup FrankenPHP in one of our GHA workflows? |
…er.php Co-authored-by: Alexander M. Turek <me@derrabus.de>
…er.php Co-authored-by: Alexander M. Turek <me@derrabus.de>
* @return $this | ||
*/ | ||
public function sendHeaders(): static | ||
public function sendHeaders(/* ?int $statusCode = null */): static | ||
{ | ||
if ($this->headersSent) { | ||
return $this; | ||
} | ||
|
||
$this->headersSent = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
won't this prevent 103 from working with streamed responses?
/** | ||
* Tracks headers already sent in informational responses | ||
*/ | ||
private array $sentHeaders; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would exposing a getter improve testability of this?
// headers | ||
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { | ||
$previousValues = $this->sentHeaders[$name] ?? null; | ||
if ($previousValues === $values) { | ||
// Header already sent in a previous response, it will be automatically copied in this response by PHP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it will be automatically copied in this response by PHP
which part does that? frakenphp?
what if we send the headers again?
it would make sense to me, to keep things simple on our side. Up to the SAPI to do more if it wants to.
at least for 103 we must repeat the same headers in the >= 200 response (and I know no other 1xx that expect headers.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes FrankenPHP does that automatically (it delegates to Go which does it automatically too). I don't think it's necessary to introduce more complexity yet because FrankenPHP is currently the only SAPI supporting this feature, and if new SAPIs add it they will likely work the same way (if that's not the case, we'll still be able to patch at that time).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I see it, this behavior from FrankenPHP is introducing complexity on our side. I'd better have it not keep early headers in memory and rely on the app to resend its headers in the real response.
I feel like the current behavior is also not future-proof and is coupled to 103. But what if another 1xx status code decides to not require repeating some headers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Go and FrankenPHP do it only for 103, so it's future proof. We should probably do it only for 103 here too.
It definitely introduces some complexity on Symfony-side, but it makes it more easy for all users using PHP directly. Also, it's a bit faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably do it only for 103 here too
Definitely! I would prefer removing this special case for 103. If it's still doable, please consider my modest request :)
The complexity this requires on our side says why, no need for more arguments.
$previousValues = null; | ||
} | ||
|
||
$newValues = null === $previousValues ? $values : array_diff($values, $previousValues); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see my previous comment: why not keep things simple and sent all headers again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With FrankenPHP is you don't do that headers will be duplicated.
} | ||
|
||
/** | ||
* @internal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be removed, the method is private
This patch adds support for sending informational responses, including Early Hints responses if supported by the SAPI. It also allows sending other informational status codes such as 102 Processing.
According to Shopify and Cloudflare, using Early Hints, the performance improvement to the Largest Contentful Paint can go from several hundred milliseconds, and up to a second faster.
Usage:
With this patch, HttpFoundation will leverage the
headers_send()
function provided by FrankenPHP. FrankenPHP is currently the only SAPI supporting Early Hints, but other SAPI such as mod_apache will probably implement this function at some point: php/php-src#7025 (comment)The low-level API is similar to the one provided by Go: golang/go#42597
The high-level API helper in
AbstractController
is similar to Node's one: nodejs/node#44180