node icon indicating copy to clipboard operation
node copied to clipboard

The `http.Server` adds a `Transfer-Encoding: chunked` header when the response has no body

Open ijisol opened this issue 1 year ago • 5 comments

Version

22 (but maybe every version)

Platform

Microsoft Windows NT 10.0.22631.0 x64 (but maybe not platform-specific)

Subsystem

node:http

What steps will reproduce the bug?

The Transfer-Encoding: chunked header is always added when there is no response body. There seems to be no way to remove it.

However, since this header is not added when the request method is HEAD, the response headers are different when the request method is GET and HEAD, which is against the HTTP specification.

Example server code:

import { createServer } from 'node:http';
import { finished } from 'node:stream/promises';

createServer(function (req, res) {
  finished(req.resume()).then(() => {
    res.writeHead(200, { 'Cache-Control': 'no-store' });
    res.end(); // No body
  }).catch((err) => {
    this.emit('error', err);
  })
}).listen(8000, '0.0.0.0');

GET:

$ curl -i http://localhost:8000/
HTTP/1.1 200 OK
Cache-Control: no-store
Date: Sat, 23 Nov 2024 09:01:22 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

HEAD:

$ curl --head -i http://localhost:8000/
HTTP/1.1 200 OK
Cache-Control: no-store
Date: Sat, 23 Nov 2024 09:01:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5

A monkey patching solution I found is to always add the Content-Length: 0 header.

However, I'm not sure if this is really the intended behavior. I think it should be possible to send a response without adding a Content-Length: 0 header when there is no body.

Or, if it's intended behavior, it should be documented.

How often does it reproduce? Is there a required condition?

Always.

What is the expected behavior? Why is that the expected behavior?

I would guess that the generally expected behavior is that the Transfer-Encoding: chunked header is NOT added when there is no response body.

What do you see instead?

.

Additional information

Perhaps this issue https://github.com/denoland/deno/issues/20063 may be related.

ijisol avatar Nov 23 '24 09:11 ijisol

Just to comment on this

Platform Microsoft Windows NT 10.0.22631.0 x64 (but maybe not platform-specific)

I've tested on Ubuntu and I see the same behavior, so I'd say this is not platform-specific

StefanStojanovic avatar Nov 25 '24 10:11 StefanStojanovic

It seems Node.js will add Transfer-Encoding: chunked by default if you do not set Content-Length or Transfer-Encoding header. You can call res.removeHeader("Transfer-Encoding") to remove this header.

theanarkh avatar Nov 26 '24 16:11 theanarkh

It seems Node.js will add Transfer-Encoding: chunked by default if you do not set Content-Length or Transfer-Encoding header. You can call res.removeHeader("Transfer-Encoding") to remove this header.

Example server:

import { createServer } from 'node:http';
import { finished } from 'node:stream/promises';

createServer(function (req, res) {
  finished(req.resume()).then(() => {
    res.removeHeader('Transfer-Encoding'); // Remove the Transfer-Encoding header
    res.writeHead(200, { 'Cache-Control': 'no-store' });
    res.end(); // No body
  }).catch((err) => {
    this.emit('error', err);
  });
}).listen(8000, '0.0.0.0');

GET request:

$ curl -i http://localhost:8000/
HTTP/1.1 200 OK
Cache-Control: no-store
Date: Tue, 26 Nov 2024 16:28:39 GMT
Connection: keep-alive
Keep-Alive: timeout=5

HEAD request:

$ curl --head -i http://localhost:8000/
HTTP/1.1 200 OK
Cache-Control: no-store
Date: Tue, 26 Nov 2024 16:28:51 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Yes, it works. But I think this is still a behavior that is hard to predict and hard to expect.

Wouldn't it be better to add the Transfer-Encoding header only when a response body exists?

ijisol avatar Nov 26 '24 16:11 ijisol

@theanarkh

It seems Node.js will add Transfer-Encoding: chunked by default if you do not set Content-Length or Transfer-Encoding header. You can call res.removeHeader("Transfer-Encoding") to remove this header.

HEAD request:

$ curl --head -iv http://localhost:8000/
* Host localhost:8000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8000...
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000
* using HTTP/1.x
> HEAD / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.10.1
> Accept: */*
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Cache-Control: no-store
Cache-Control: no-store
< Date: Thu, 28 Nov 2024 04:47:49 GMT
Date: Thu, 28 Nov 2024 04:47:49 GMT
< Connection: keep-alive
Connection: keep-alive
< Keep-Alive: timeout=5
Keep-Alive: timeout=5
<

* Connection #0 to host localhost left intact

GET request:

$ curl -iv http://localhost:8000/
* Host localhost:8000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8000...
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000
* using HTTP/1.x
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.10.1
> Accept: */*
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Cache-Control: no-store
Cache-Control: no-store
< Date: Thu, 28 Nov 2024 04:47:56 GMT
Date: Thu, 28 Nov 2024 04:47:56 GMT
< Connection: keep-alive
Connection: keep-alive
< Keep-Alive: timeout=5
Keep-Alive: timeout=5
* no chunk, no close, no size. Assume close to signal end
<

* abort upload
* shutting down connection #0

When I printed the details with curl, it seems that the response is not closed properly when res.removeHeader("Transfer-Encoding") is used. This seems like a solution that shouldn't be used.

ijisol avatar Nov 28 '24 04:11 ijisol

Yeah i think you need to set content length header, havent had this problem

Realrubr2 avatar Dec 05 '24 17:12 Realrubr2