Problem/Motivation

#3295790: Post-response task running (destructable services) are actually blocking; add test coverage and warn for common misconfiguration adds a content-length header to most responses coming from Drupal. It needs to be adjust due the expectations laid in https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length. Among these are:

  • For HEAD requests content-length should be equal to the equivalent GET request - tested this and it works so that's good.
  • 1xx and 204 cannot have a content length header
  • 304s has a complex rule about being the same as a 200

Also there are some other things we should account for based on the issues we had when Symfony set a content-length header on everything. See https://github.com/symfony/symfony/issues/46449.

  • No content-length header when the transfer-encoding header is set.
  • No content-length header when $response->getContent() returns FALSE.

Lastly we should not add the header to 500s as this causes issues with if there is an error during batch processing.

Steps to reproduce

Proposed resolution

Remaining tasks

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

CommentFileSizeAuthor
#9 3396559-nr-bot.txt90 bytesneeds-review-queue-bot

Issue fork drupal-3396559

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

alexpott created an issue. See original summary.

alexpott’s picture

Status: Active » Needs review

This changes the method to look like this:

  public function setContentLengthHeader(ResponseEvent $event): void {
    $response = $event->getResponse();
    if ($response instanceof StreamedResponse) {
      return;
    }

    // Do not add header for 100s and 500s, see
    // https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length.
    if ($response->isInformational() || $response->isServerError() || $response->isEmpty()) {
      return;
    }

    if ($response->headers->has('Transfer-Encoding')) {
      return;
    }

    $content = $response->getContent();
    if ($content === FALSE) {
      return;
    }

    $response->headers->set('Content-Length', strlen($content), TRUE);
  }

Some of this is inspired by what's in \Symfony\Component\HttpFoundation\Response::prepare() which is the last thing called by \Drupal\Core\DrupalKernel::handle() before we send the request.

So as it turns out Symfony is already fixing quite a bit of this for us, but I still think we shouldn't be adding this header in when we know we're just going to remove it later.

One thing I wonder is just we remove

    if ($response instanceof StreamedResponse) {
      return;
    }

as this is covered by

    $content = $response->getContent();
    if ($content === FALSE) {
      return;
    }
andypost’s picture

Curious how it will work when gzip/brotli will be enabled at web-server side

alexpott’s picture

@andypost how is that relevant to this issue? Do they require the content-length header? FWIW server side gzip/brotli must adjust the content-length header - as suggested here https://serverfault.com/questions/783029/apache-returns-invalid-content-...

catch’s picture

So as it turns out Symfony is already fixing quite a bit of this for us, but I still think we shouldn't be adding this header in when we know we're just going to remove it later.

Yes this makes sense, as evidenced by having to get to this issue to find this out. Removing the explicit StreamedResponse check seems fine, if that somehow changes later, we'll find out.

alexpott’s picture

I'm thinking that we should separate out the isServerError() check as that's the one thing that's not per RFC but down to some oddness in Drupal's error handling. Then if we fix that we could remove it. Pushed that change to the MR.

alexpott’s picture

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Looks like #3409885: Uncaught ajax.js error / exception is reporting this too.

catch’s picture

There's one case in #3409885-15: Uncaught ajax.js error / exception that this won't fix, but it's not clear to me how they ended up in that situation. Given that, I think we should go ahead here to at least now the failure cases down more.

catch’s picture

Status: Needs work » Reviewed & tested by the community

I only pushed the rebase button to get the MR green again. This looks fine to me and I think we should go ahead.

larowlan’s picture

Issue tags: +Patch release target
larowlan’s picture

Issue credits

  • larowlan committed c2090141 on 10.2.x
    Issue #3396559 by alexpott, catch: Only set content-length header in...

  • larowlan committed f47d1d15 on 11.x
    Issue #3396559 by alexpott, catch: Only set content-length header in...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Patch release target

Committed to 11.x and backported to 10.2.x

Thought about asking for a test-case with StreamedResponse with a callback, but then realized StreamedResponse::getContent returns FALSE so it doesn't matter.

Thanks all

mstrelan’s picture

This seems to have introduced a new test class with a typo, we now have FinishResponseSubscriberTest and FinishResponserSubscriberTest. Should this be reverted and we merge these together?

larowlan’s picture

#3410022: Regression from #3295790 content-length header set earlier than expected deletes that test and is also critical

Should we focus effort there?

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.