Skip to content

Almost guaranteed ECONNRESET on piped sockets if connecting to Node's HTTPS server which answers with "connection: 'close'" after setImmediate or setTimeout, on OSX #23169

Open
@decadent

Description

@decadent
  • Version: v10.11.0
  • Platform: Darwin localhost.local 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 21 20:07:40 PDT 2018; root:xnu-3248.73.11~1/RELEASE_X86_64 x86_64 (aka OSX 10.11.6, also happens on High Sierra)
  • Subsystem: ???

In the following code, pem is a module from NPM, and the rest is an HTTP proxy which serves CONNECT requests and pipes them to an HTTPS server defined in the same script:

const http = require('http');
const https = require('https');
const pem = require('pem');
const net = require('net');

const createHttpsServer = (callback) => {
  pem.createCertificate({
    days: 365,
    selfSigned: true
  }, (error, {serviceKey, certificate, csr}) => {
    const server = https.createServer({
      ca: csr,
      cert: certificate,
      key: serviceKey
    }, (req, res) => {
      setImmediate(() => {
        res.writeHead(200, {
          connection: 'close'
        });
        res.end('OK');
      });
    });

    server.listen((error) => {
      if (error) {
        console.error(error);
      } else {
        callback(null, server.address().port);
      }
    });
  });
};

const createProxy = (httpsServerPort) => {
  const proxy = http.createServer();
  proxy.on('connect', (request, requestSocket, head) => {
    const serverSocket = net.connect({
      port: httpsServerPort
    }, 'localhost', () => {
      requestSocket.write(
        'HTTP/1.1 200 Connection established\r\n\r\n'
      );

      serverSocket.write(head);
      serverSocket.pipe(requestSocket);
      requestSocket.pipe(serverSocket);
    });
  });

  proxy.listen(9000);
};

createHttpsServer((error, httpsServerPort) => {
  if (error) {
    console.error(error);
  } else {
    createProxy(httpsServerPort);
  }
});

If you run curl --proxy http://localhost:9000 https://qweasd/ -k, you'll see Curl receive the reply, and meanwhile the script will almost certainly fail with:

events.js:167
      throw er; // Unhandled 'error' event
      ^

Error: read ECONNRESET
    at TCP.onStreamRead (internal/stream_base_commons.js:111:27)
Emitted 'error' event at:
    at Socket.onerror (_stream_readable.js:690:12)
    at Socket.emit (events.js:182:13)
    at emitErrorNT (internal/streams/destroy.js:82:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:50:3)
    at process._tickCallback (internal/process/next_tick.js:63:19)

This issue is dependent on several parameters:

  • You're running OSX. El Capitan and High Sierra both exhibit this, an Ubuntu VM doesn't.
  • The server is HTTPS: an HTTP server and curl --proxytunnel work alright.
  • The server is running in the same script as the proxy.
  • The server answers with a connection: 'close' header.
  • The strangest one: the server request handler has setImmediate or setTimeout around the reply (with, I guess, any timeout value). process.nextTick doesn't do it. And if the handler instead serves the reply immediately, the server continues to answer perfectly alright until you hit a seemingly different ECONNRESET.

If you adorn the sockets with error event handlers, you'll see that the errors actually don't happen on each request, and when they do, it's primarily on the outgoing socket from the proxy to the HTTPS server; and often, but not necessarily, a second ECONNRESET occurs on the incoming socket to the proxy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedIssues that need assistance from volunteers or PRs that need help to proceed.httpIssues or PRs related to the http subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions