Navigation Menu

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

Add headers_send_early_and_clear() function for HTTP Early Hints #7025

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

nikic
Copy link
Member

@nikic nikic commented May 21, 2021

This is intended to be used in conjunction with HTTP Early Hints
and other 1xx status codes, which require sending multiple sets
of headers.

Usage:

header('HTTP/1.1 103 Early Hints');
header('Link: </style.css>; rel=preload; as=style');
headers_send_early_and_clear();

// Normal code goes here. You can send more headers and body
// here.

This is intended to be used in conjunction with HTTP Early Hints
and other 1xx status codes, which require sending multiple sets
of headers.

Usage:

    header('HTTP/1.1 103 Early Hints');
    header('Link: </style.css>; rel=preload; as=style');
    headers_send_early_and_clear();

    // Normal code goes here. You can send more headers and body
    // here.
@nikic
Copy link
Member Author

nikic commented May 21, 2021

cc @nicolas-grekas

@bwoebi
Copy link
Member

bwoebi commented May 21, 2021

I have to admit I was thinking about "early and clear" sending when reading the signature which confused me (what's "clear sending"?) until seeing the code. Maybe it just should be called headers_flush()?

@nikic
Copy link
Member Author

nikic commented May 21, 2021

My original name was headers_send() and I did a last-minute rename to make it extra ... ehem ... clear. Guess I failed :P

I'd like to have something that makes it obvious that this only sends one set of headers, and you can send more afterwards. headers_flush() sounds like it will just send the normal headers early, instead of waiting for first output.

@bwoebi
Copy link
Member

bwoebi commented May 21, 2021

headers_flush() sounds like it will just send the normal headers early, instead of waiting for first output.

But … that's what it does? Or do you mean the autogenerated headers which are sent on first byte if not set yet?
I never keep these automatic headers on my mind, I'm just considering that a SAPI feature to set these necessary headers if not overwritten on the first byte. I did not consider them flushable or anything like that, just to be sent at the latest possible point.

@nikic
Copy link
Member Author

nikic commented May 21, 2021

headers_flush() sounds like it will just send the normal headers early, instead of waiting for first output.

But … that's what it does?

No, this allows you to send multiple independent sets of headers, not just flush a single set early.

@bwoebi
Copy link
Member

bwoebi commented May 21, 2021

Yes exactly, just like on a stream you can flush multiple times. (until the stream is closed, or in this case the first data byte sent)

@nicolas-grekas
Copy link
Contributor

headers_flush() would work for me. Maybe we should make it accept an argument: the status code?
That would prevent an undefined behavior: what happens on the 2nd call to headers_flush() when no new status header has been defined?

@nikic
Copy link
Member Author

nikic commented May 25, 2021

That would prevent an undefined behavior: what happens on the 2nd call to headers_flush() when no new status header has been defined?

The function resets all headers (including status), so you'll get the default status of 200, unless it is explicitly changed.

@dunglas
Copy link
Contributor

dunglas commented May 25, 2021

According to the RFC, the headers sent in the early response must also be included in the final response to be taken into account.

While your proposal allows to do this, it means that the user must not forget to re-add the headers in the final response. In Go (golang/go#42597) we followed another approach: headers are preserved by default, but can be removed if needed. Wouldn't a similar approach be more user friendly in PHP too? Having two different functions would allow to do it easily. Example:

// Typical case

header('Link: </style.css>; rel=preload; as=style');
headers_send(103);

// Result: the Link header will also be included in the final response:
//
// HTTP/1.1 103 Early Hints
// Link: </style.css>; rel=preload; as=style
//
// HTTP/1.1 200 OK
// Link: </style.css>; rel=preload; as=style
// ...
// More advanced case

header('Link: </style.css>; rel=preload; as=style');
headers_send(103);

headers_clear();
headers_send(500);
// Result: the Link header will not be included in the final response and will be discarded by the client
//
// HTTP/1.1 103 Early Hints
// Link: </style.css>; rel=preload; as=style
//
// HTTP/1.1 500 OK
// ... [no Link header]

WDYT?

@nicolas-grekas
Copy link
Contributor

headers are preserved by default, but can be removed if needed

I like that!
removing can be done with header_remove()

Since no HTTP RFC has a use case for not sending the same headers, no need to add headers_clear() actually

@dunglas
Copy link
Contributor

dunglas commented May 25, 2021

@nikic, I wonder if this patch really works:

This works for sure (but maybe by accident) with HTTP/1. However, with HTTP/2 and HTTP/3 things get harder: HTTP/2 and 3 are binary protocols. Responses must be sent using "special" binary frames (for instance, here is the Go implementation of the 103 status code for HTTP/3: quic-go/quic-go#3047).

According to the CGI spec (which is inherited by FastCGI), a CGI script can only send one response, and this response cannot have a 1XX status code.

I fear that with the current implementation, the web server will send the second (final) response as part of the body frame instead of being "converted" in a new response frame. I've not tested this, but from my understanding of the spec, I don't see how it could behave differently. While it's not an issue with HTTP/1.1 (because it's a text protocol), it will break sites using HTTP/2 or 3.

@nicolas-grekas
Copy link
Contributor

@mnot would you have some advice for us here? We might need an update to rfc3875 maybe, or to fastcgi?

@yoavweiss
Copy link

^^ @kazuho

@mnot
Copy link

mnot commented May 26, 2021

So, CGI doesn't constraint HTTP/1.1; it just constraints resources that use it. It may be that PHP hosted by FastCGI might not have the ability to send a 1xx response, but I think documenting that limitation should be sufficient. If there's wide enough interest, CGI could be updated to support non-final responses, but that depends mostly on implementation interest...

WRT the headers_send(103) approach above - I like it.

@nicolas-grekas
Copy link
Contributor

nicolas-grekas commented May 26, 2021

The issue is that FastCGI is by far the most widespread way to run a PHP web app.

Updating CGI to 1.2 to add support for 1xx responses looks quite desired from this end. What's the best place to talk about this and let implementers know about it, or just ask them?

On the php-fpm side, headers_send(103) could then be a no-op when GATEWAY_INTERFACE is < 1.2.
On the Apache SAPI, we could maybe make it work right away.
headers_send() could return a boolean to tell if it did anything btw!

A pure php-side alternative might be to make php-fpm talk HTTP instead of fastcgi (in addition to actually). But then, we'll loose all the nice integration that CGI provides thanks to meta-variables, which could be a blocker for the community.

For the record, because it took me some time to figure this out, CGI clients are expected to parse the response from the CGI server (PHP) and to "fix" the headers, like turning Status: 200 into a proper HTTP/* line, adding any missing Date header, etc. If we hack with CGI 1.1, CGI clients might eg add this Date header to the 1xx response, which would be wrong. I suppose that for CGI 1.2, clients should instead be forbidden from adding/parsing any headers of 1xx responses but the Status one.

@mnot
Copy link

mnot commented Jul 1, 2021

@RoUS any thoughts? CGI/1.2 was started but never finished; maybe time to pick it up again?

@bukka
Copy link
Member

bukka commented Jul 21, 2021

I guess the bigger challenge would be then to update the FastCGI spec which is not really standardized anywhere that I know. We partially (not all features are implemented) follow what's here but that's just archive as the main site is no longer available. It would be quite useful to have a real standard for it though.

@mnot
Copy link

mnot commented Jul 22, 2021

FastCGI isn't really maintained, so I think it's reasonable for PHP to prototype an extension and perhaps talk to other implementations about it.

@alexander-schranz
Copy link

Would be interesting to see this in PHP as now dunglas have add it to GoLang I think we maybe will see it soon supported by webserver like caddy: caddyserver/caddy#4860

@RoUS
Copy link

RoUS commented Oct 11, 2022

@mnot

@RoUS any thoughts? CGI/1.2 was started but never finished; maybe time to pick it up again?

I've actually been thinking about that sporadically. The main thing I think a new spec of CGI could bring to the table is channels beyond stdin, stdout, and stderr — in particular, a control channel so that the script can submit progress reports (like HTTP 100 Continue) while working.

I haven't yet gotten a handle on how to make that portable, though. Hmm.

@dunglas
Copy link
Contributor

dunglas commented Nov 6, 2022

For the record, Early Hints have been implemented in FrankenPHP. This SAPI provides a headers_send() function behaving as described in #7025 (comment).

nicolas-grekas added a commit to symfony/symfony that referenced this pull request Mar 13, 2023
…Early Hints) and other 1XX statuses (dunglas)

This PR was squashed before being merged into the 6.3 branch.

Discussion
----------

[HttpFoundation] Add support for the 103 status code (Early Hints) and other 1XX statuses

| Q             | A
| ------------- | ---
| Branch?       | 6.3
| Bug fix?      | no
| New feature?  | yes <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | yes <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | n/a <!-- prefix each issue number with "Fix #", no need to create an issue if none exists, explain below instead -->
| License       | MIT
| Doc PR        | todo

This patch adds support for sending informational responses, including [Early Hints responses](https://developer.chrome.com/blog/early-hints/) if supported by the SAPI. It also allows sending other informational status codes such as 102 Processing.

According to [Shopify](https://twitter.com/colinbendell/status/1539322190541295616) and [Cloudflare](http://blog.cloudflare.com/early-hints-performance), using Early Hints, the performance improvement to the Largest Contentful Paint can go from several hundred milliseconds, and up to a second faster.

Usage:

```php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
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(): 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);
    }
}
```

With this patch, HttpFoundation will leverage the `headers_send()` function provided by [FrankenPHP](https://frankenphp.dev). 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

Commits
-------

5be52b2 [HttpFoundation] Add support for the 103 status code (Early Hints) and other 1XX statuses
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.

None yet