microsoft / microsoft/vscode-remote-release
DevContainer Port Forwarding terminates prematurely
@chrmarti is already working on this.
Since Jun 19, 2026.
- Dominant language
- Dockerfile
- Stars
- 4.2k
- Forks
- 469
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 1
Description
- VSCode Version: Version: 1.125.0 (Universal)
- Local OS Version: macOS 26.0
- Remote OS Version: Debian GNU/Linux 13 (trixie)
- Remote Extension/Connection Type: Dev Containers (local via podman + lima)
- Logs: Chrome: "net::ERR_INCOMPLETE_CHUNKED_ENCODING", curl: "curl: (18) transfer closed with outstanding read data remaining"
Steps to Reproduce:
HTTP requests served with "Transfer-Encoding: chunked" are randomly not transmitted in full from container to host (because the bridge terminates prematurely).
Below I'm attaching a nodeJs server and client script to reproduce.
- run
node server.jsinside the devcontainer. - run
node client.jsoutside the devcontainer on the host with port forwarding in vscode enabled. - The client will encounter a varying number of ECONNRESET errors (for me between 25 and 40 in 200 requests)
Does this issue occur when you try this locally?: With local containers: Yes, Locally without containers: No, With client + server both inside the container: No
Does this issue occur when you try this locally and all extensions are disabled?: Did not try - I successfully fixed the bug by fixing the compiled ms-vscode-remote.remote-containers extension on my machine.
server.js
const http = require('http');
const { Readable } = require('stream');
const PORT = parseInt(process.env.PORT || '3030', 10);
const CHUNK_SIZE = parseInt(process.env.CHUNK_SIZE || '16384', 10);
const TOTAL_BYTES = parseInt(process.env.TOTAL_BYTES || String(8 * 1024 * 1024), 10);
const SENTINEL = '__END_OF_STREAM__';
const EXPECTED_BODY_BYTES = TOTAL_BYTES + Buffer.byteLength(SENTINEL);
const CHUNK = Buffer.alloc(CHUNK_SIZE, 0x41 /* 'A' */);
// Generator-backed body for chunked transfer
function* body() {
for (let sent = 0; sent < TOTAL_BYTES; sent += CHUNK_SIZE) {
yield sent + CHUNK_SIZE <= TOTAL_BYTES ? CHUNK : CHUNK.subarray(0, TOTAL_BYTES - sent);
}
yield Buffer.from(SENTINEL);
}
http
.createServer((req, res) => {
req.resume(); // drain any request body
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Cache-Control': 'no-store',
'Transfer-Encoding': 'chunked',
'X-Expected-Body-Bytes': String(EXPECTED_BODY_BYTES),
// Close after each response so every request exercises the bridge's
// connection-close handling — where the truncation occurs.
'Connection': 'close',
});
Readable.from(body()).pipe(res);
})
.listen(PORT, '0.0.0.0', () => {
console.log(
`well-behaved chunked server listening on 0.0.0.0:${PORT} | ` +
`total=${TOTAL_BYTES} bytes | chunk=${CHUNK_SIZE} | expectedBody=${EXPECTED_BODY_BYTES}`
);
});
client.js
const http = require('http');
const HOST = process.env.HOST || '127.0.0.1';
const PORT = parseInt(process.env.PORT || '3030', 10);
const COUNT = parseInt(process.env.COUNT || '200', 10);
const SENTINEL = '__END_OF_STREAM__';
const SENTINEL_LEN = Buffer.byteLength(SENTINEL);
let failures = 0;
let truncations = 0;
let errors = 0;
function makeRequest(i) {
return new Promise((resolve) => {
const req = http.get(
{ host: HOST, port: PORT, path: '/', headers: { Connection: 'close' } },
(res) => {
const expected = parseInt(res.headers['x-expected-body-bytes'] || '0', 10);
const chunks = [];
let aborted = false;
res.on('data', (c) => chunks.push(c));
// Node emits 'aborted' / an error with code ERR_INCOMPLETE_CHUNKED_ENCODING
// when a chunked response is closed before the terminating chunk arrives.
res.on('aborted', () => {
aborted = true;
});
res.on('error', (err) => {
aborted = true;
errors++;
failures++;
console.error(`[${i}] RES ERROR ${err.code || err.message}`);
});
res.on('end', () => {
const body = Buffer.concat(chunks);
const tail = body.subarray(Math.max(0, body.length - SENTINEL_LEN)).toString('latin1');
const sentinelOk = tail === SENTINEL;
const lengthOk = body.length === expected;
if (aborted || !sentinelOk || !lengthOk) {
failures++;
truncations++;
console.error(
`[${i}] TRUNCATED got=${body.length} expected=${expected} ` +
`sentinelOk=${sentinelOk} aborted=${aborted}`
);
}
resolve();
});
res.on('close', () => {
if (aborted && chunks.length >= 0) {
// 'end' may not fire on a hard abort; ensure we still resolve.
resolve();
}
});
}
);
req.on('error', (err) => {
errors++;
failures++;
console.error(`[${i}] REQ ERROR ${err.code || err.message}`);
resolve();
});
});
}
async function main() {
console.log(`Requesting http://${HOST}:${PORT}/ x${COUNT} ...`);
const start = Date.now();
for (let i = 0; i < COUNT; i++) {
await makeRequest(i);
}
const secs = ((Date.now() - start) / 1000).toFixed(1);
console.log('-----------------------------------------------');
console.log(`Done in ${secs}s | requests=${COUNT}`);
console.log(`failures=${failures} (truncations=${truncations}, errors=${errors})`);
if (failures > 0) {
console.log('REPRODUCED: at least one response was truncated / prematurely closed.');
process.exitCode = 1;
} else {
console.log('No truncation observed in this run. Try increasing COUNT or TOTAL_BYTES.');
}
}
main();
devcontainer.json
{
"name": "chunked-forwarding-repro",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"forwardPorts": [3030],
"portsAttributes": {
"3030": {
"label": "chunked-server",
"onAutoForward": "silent"
}
},
"postCreateCommand": "node --version"
}
The bug is in the inline script that the extension runs in the container.
Faulty code in the devcontainers extension:
const net = require('net');
const fs = require('fs');
process.stdin.pause();
const client = net.createConnection({ host: '127.0.0.1', port: ${o} }, () => {
console.error('Connection established');
client.pipe(process.stdout);
process.stdin.pipe(client);
});
client.on('close', function (hadError) {
console.error(hadError ? 'Remote close with error' : 'Remote close');
process.exit(hadError ? 1 : 0); // <<<- This does not wait for the pipe to drain
});
client.on('error', function (err) {
process.stderr.write(err && (err.stack || err.message) || String(err));
});
process.stdin.on('close', function (hadError) {
console.error(hadError ? 'Remote stdin close with error' : 'Remote stdin close');
process.exit(hadError ? 1 : 0);
});
process.on('uncaughtException', function (err) {
fs.writeSync(process.stderr.fd, \`Uncaught Exception: \${String(err && (err.stack || err.message) || err)}\\n\`);
});
Verified fix:
const net = require('net');
const fs = require('fs');
process.stdin.pause();
const client = net.createConnection({ host: '127.0.0.1', port: ${o} }, () => {
console.error('Connection established');
client.pipe(process.stdout);
process.stdin.pipe(client);
});
client.on('close', function (hadError) {
console.error(hadError ? 'Remote close with error' : 'Remote close');
// Wait for drainage
if (!process.stdout.writableEnded && !process.stdout.destroyed) {
process.stdout.end();
}
if (process.stdout.writableFinished) {
process.exit(hadError ? 1 : 0);
}
Promise.race([
new Promise(resolve => process.stdout.once('finish', resolve)),
new Promise((_, reject) => process.stdout.once('error', reject)),
]).then(() => process.exit(hadError ? 1 : 0),() => process.exit(1));
});
client.on('error', function (err) {
process.stderr.write(err && (err.stack || err.message) || String(err));
});
process.stdin.on('close', function (hadError) {
console.error(hadError ? 'Remote stdin close with error' : 'Remote stdin close');
process.exit(hadError ? 1 : 0);
});
process.on('uncaughtException', function (err) {
fs.writeSync(process.stderr.fd, \`Uncaught Exception: \${String(err && (err.stack || err.message) || err)}\\n\`);
});
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.