Repository: fastify/fastify (JavaScript)

Issue:
After changing payload in onSend hook, Content-Length is not updated
### Prerequisites

- [X] I have written a descriptive issue title
- [X] I have searched existing issues to ensure it has not already been reported


### Fastify version

3.15.1

### Node.js version

14.16.1

### Operating system

Linux

### Operating system version (i.e. 20.04, 11.3, 10)

20.04

### Description

I'm minifying payloads in onSend. Without explicitly setting Content-Length, the old Content-Length of the unmodified payload is sent. Probably the other relevant detail is that the payloads are generated by fastify-static and they use streams, so I'm draining the stream into a string which I then modify.

### Steps to Reproduce

Here's the entire plugin:

```typescript
'use strict';

import fastifyPlugin from 'fastify-plugin';
import {FastifyPluginAsync} from 'fastify';
import htmlMinifier from 'html-minifier';
import {PassThrough} from 'readable-stream';

const streamToString = (stream: any): Promise<string> => {
  const chunks: Buffer[] = [];
  return new Promise((resolve, reject) => {
    stream.on('data', (chunk: any) => chunks.push(Buffer.from(chunk)));
    stream.on('error', (err: Error) => reject(err));
    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  });
};

const minify = (s: string) =>
  htmlMinifier.minify(s, {
    collapseBooleanAttributes: true,
    collapseWhitespace: true,
    minifyCSS: true,
    minifyJS: true,
    removeComments: true,
    removeEmptyAttributes: true,
    removeRedundantAttributes: true,
    removeScriptTypeAttributes: true,
    removeStyleLinkTypeAttributes: true,
    useShortDoctype: true,
  });

const plugin: FastifyPluginAsync = async (app, _options) => {
  app.addHook('onSend', async (_request, reply, payload) => {
    const contentType = reply.getHeader('Content-Type');
    if (contentType && contentType.includes('text/html')) {
      if (payload instanceof PassThrough) {
        const content = minify(await streamToString(payload));
        //reply.header('Content-Length', content.length);
        return content;
      }
    }
  });
};

export default fastifyPlugin(plugin);
```

### Expected Behavior

Content-Length should be set based on the new payload.


Discussion:
Good spot! I think we should just set it once after we run onSend.


Would you like to send a Pull Request to address this issue? Remember to add unit tests.
It's probably a little complicated for what I have time for right now especially since I can work around it. I tried the following, which solves my problem, but causes a test failure. It was HEAD related so maybe just adding a check against method, but I don't think I'm comfortable enough with the code to know what's correct here.

```diff
diff --git a/lib/reply.js b/lib/reply.js
index 1178b6f..daca217 100644
--- a/lib/reply.js
+++ b/lib/reply.js
@@ -426,9 +426,7 @@ function onSendEnd (reply, payload) {
     throw new FST_ERR_REP_INVALID_PAYLOAD_TYPE(typeof payload)
   }
 
-  if (!reply[kReplyHeaders]['content-length']) {
-    reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
-  }
+  reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
 
   reply[kReplySent] = true
```



Patch:
diff --git a/lib/reply.js b/lib/reply.js
index 1178b6fa81c..555b09941e6 100644
--- a/lib/reply.js
+++ b/lib/reply.js
@@ -428,6 +428,8 @@ function onSendEnd (reply, payload) {
 
   if (!reply[kReplyHeaders]['content-length']) {
     reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
+  } else if (req.raw.method !== 'HEAD' && reply[kReplyHeaders]['content-length'] !== Buffer.byteLength(payload)) {
+    reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
   }
 
   reply[kReplySent] = true


Test patch:
diff --git a/test/hooks.test.js b/test/hooks.test.js
index 5dfaa911e00..539161e20d1 100644
--- a/test/hooks.test.js
+++ b/test/hooks.test.js
@@ -1334,6 +1334,30 @@ test('onSend hook should receive valid request and reply objects if a custom con
   })
 })
 
+test('Content-Length header should be updated if onSend hook modifies the payload', t => {
+  t.plan(2)
+
+  const instance = Fastify()
+
+  instance.get('/', async (_, rep) => {
+    rep.header('content-length', 3)
+    return 'foo'
+  })
+
+  instance.addHook('onSend', async () => 'bar12233000')
+
+  instance.inject({
+    method: 'GET',
+    url: '/'
+  }, (err, res) => {
+    t.error(err)
+    const payloadLength = Buffer.byteLength(res.body)
+    const contentLength = Number(res.headers['content-length'])
+
+    t.equal(payloadLength, contentLength)
+  })
+})
+
 test('cannot add hook after binding', t => {
   t.plan(2)
   const instance = Fastify()