got: Server side aborted request with Transfer-Encoding: chunked never resolve/reject

  • Node.js version: 13.9.0 at least to 13.11.0
  • OS & version: Ubuntu 19.10

A server side aborted request with Transfer-Encoding: chunked header conduces got to never resolve nor reject. I think got doesn’t listen aborted event from IncomingMessage. The stream version of got is affected too and stream never emit end, abort or error but stays in a stale state.

const http = require('http');
const got = require('got');

// http server that aborts a chunked request
const server = http.createServer((req, res) => {
	res
		.on('error', (e) => {
			console.log(`server error: ${e}`)
		})
		.on('close', () => {
			console.log('server close')
		})
		.on('end', () => {
			console.log('server end')
		})
		.on('finish', () => {
			console.log('server finish')
		});

	res.writeHead(200, { 'Content-Type': 'text/plain' });
	res.write('chunk 1');

	setTimeout(() => res.write('chunk 2'), 1000);
	setTimeout(() => res.destroy(), 2000);

	// this destroy version leads to the same result
	// setTimeout(() => res.socket.destroy(), 2000);

	// a non-aborting version would call res.end() as it
	// setTimeout(() => res.end(), 2000);
});
server.listen(8000);

// got request
(async () => {
	try {
		const gotOptions = {timeout: {socket: 2000, request: 3000}};
		const res = await got('http://localhost:8000', gotOptions);
		console.log(`client res: res=${JSON.stringify(res.body)}`);
	} catch (e) {
		console.log(`client error: error=${e}`);
	}
})();

This only outputs server close but no client response because the got promise never resolves nor rejects even with timeouts set. IncomingMessage doesn’t emit end event in this case (at least in the latest node version, maybe this affirmation was not true in past versions, see: https://github.com/nodejs/node/issues/25081, from where the examples here are derived) but an aborted event. To show it, you can do that:

const http = require('http');

// http server that aborts a chunked request
const server = http.createServer((req, res) => {
	res
		.on('error', (e) => {
			console.log(`server error: ${e}`)
		})
		.on('close', () => {
			console.log('server close')
		})
		.on('end', () => {
			console.log('server end')
		})
		.on('finish', () => {
			console.log('server finish')
		});

	res.writeHead(200, { 'Content-Type': 'text/plain' });
	res.write('chunk 1');

	setTimeout(() => res.write('chunk 2'), 1000);
	setTimeout(() => res.destroy(), 2000);

	// this destroy version leads to the same result
	// setTimeout(() => res.socket.destroy(), 2000);

	// a non-aborting version would call res.end() as it
	// setTimeout(() => res.end(), 2000);
});
server.listen(8000);

// standard http request
const req = http.get('http://localhost:8000', res => {
	console.log('Status code:', res.statusCode);
	console.log('Raw headers:', res.rawHeaders);

	res
		.on('data', (data) => {
			console.log(`client data: complete=${res.complete}, aborted=${res.aborted}, data=${data.toString()}`)
		})
		.on('error', (e) => {
			console.log(`client error: complete=${res.complete}, aborted=${res.aborted}, error=${e}`)
		})
		.on('aborted', () => {
			console.log(`client aborted: complete=${res.complete}, aborted=${res.aborted}`)
		})
		.on('close', () => {
			console.log(`client close: complete=${res.complete}, aborted=${res.aborted}`)
		})
		.on('end', () => {
			console.log(`client end: complete=${res.complete}, aborted=${res.aborted}`)
		});
});

req.on('error', (e) => {
	console.log(`client error: error=${e}`);
});

Here, you can see a more precise output:

Status code: 200
Raw headers: [
 ... some headers ...
  'Transfer-Encoding',
  'chunked'
]
client data: complete=false, aborted=false, data=chunk 1
client data: complete=false, aborted=false, data=chunk 2
client aborted: complete=false, aborted=true
client close: complete=false, aborted=true
server close

Note there’s no client end event but an aborted one. I have also included two statuses properties from IncomingMessage: complete (true after an end) and aborted (true after an aborted event)

Checklist

  • I have read the documentation.
  • I have tried my code with the latest version of Node.js and Got.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 25

Most upvoted comments

Fwiw, I just stumbled upon this issue and I can reproduce it in both v13.6.0 and v13.12.0.

fwiw I landed on this issue when I encountered a resolved, chunked, response - it took some time to isolate that the transfer-encoding was the culprit because I just received a blank body.

This was on Node 14.0. When I switched to Node 12.17 given the issues noted above it works as I would have expected. The promise resolves with the fully downloaded response. I upgraded from 14.0 to the 14.5 minor and re-ran my little test and it works as expected. lol node.

I anticipate others may experience similar confusion on Node for Node 14 <= 14.5. Hoping I can save someone an hour or two trying to track that one down if they happen to stumble across this issue.

In fact, I think your problem is related to this issue: https://github.com/sindresorhus/got/issues/1295 which is a node 14 fixed issue.

If you see any bug, make an issue stright away! I’ve fixed two already and I’m going to release v11.0.1 in a few mins…

I can confirm with the brand new got v11, an error is thrown/emitted on server abort after response cases in http 1.x context. Good job @szmarczak and thank you! I need to test http 2 now!

ECONNRESET occurs when the underlying socket is destroyed before the request finish.

It occurs when the socket is closed without emitting a response event.

got v10 simply hang

Then I’m almost sure it was caused by the premature close errors, which just were ignored.

I’m really sure too for got v9: there’s no hang related to this issue, my code and tests have been written with got v9.

The bug was still present, it just was failing silently:

https://runkit.com/szmarczak/5e9858d5a67271001a07e593

Got v11 listen only two of these, error and end, so the aborted cases just hang programs.

not anymore (wait for a release)

It should not be handled by timeouts and throws a timeout error, even if promises now resolve/reject or streams emit error event with got v11 (that is a good thing) because it’s not a timeout. If timeouts are set at an upper value, say a minute as an example, the error will not be caught and transmitted until. There is a true event when this occurs: the aborted one (as node doc says: https://nodejs.org/api/http.html#http_event_aborted). There is no error or end event emitted by node’s http module (there’s a close one, but it possibly depends on emitClose streams’ param). The fact there is a specific event for that and not a simple true error is out of our scope, and maybe be submitted to node’s team.

In got v11 it should be there: https://github.com/sindresorhus/got/blob/bf0b0cde07b6b65b18601aa3f43f976f4150c1f2/source/core/index.ts#L962-L969 You have to add an additional listener on this like:

response.on('aborted', () => {
	// there is no error with this event, so create one
	// (it should be improved to have a specific error with an error code, etc...)
	const error = new Error('Request abort');
	this._beforeError(new ReadError(error, options, response as Response));
});

With that, the output is:

client error: error=ReadError: Request abort
server close

which is better, doesn’t depends on timeouts settings and is handleable by applications.

I just tried the v11 beta. Seems to be working for me now 👍