Repository: koajs/koa (js)

Issue:
pipeling request will lost!
### [Test code](https://gist.github.com/zensh/02f6e665cd7eead9b2e072ce4194a4bf)

### Test result:
```
=== Http server ===
Request count: 1 , Error listeners: 1
Request count: 2 , Error listeners: 1
HTTP/1.1 200 OK
Date: Wed, 20 Jul 2016 02:56:47 GMT
Connection: keep-alive
Content-Length: 1

1HTTP/1.1 200 OK
Date: Wed, 20 Jul 2016 02:56:47 GMT
Connection: keep-alive
Content-Length: 1

2

=== Toa server ===
Request count: 1 , Error listeners: 1
Request count: 2 , Error listeners: 1
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 1
X-Powered-By: Toa
Date: Wed, 20 Jul 2016 02:56:47 GMT
Connection: keep-alive

1HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 1
X-Powered-By: Toa
Date: Wed, 20 Jul 2016 02:56:47 GMT
Connection: keep-alive

2

=== Koa server ===
Request count: 1 , Error listeners: 2
Request count: 2 , Error listeners: 2
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 1
Date: Wed, 20 Jul 2016 02:56:47 GMT
Connection: keep-alive

1
```

### Reason

When server emit a `request`, `responce` instance may not have `socket` for the time being, [_http_server.js](https://github.com/nodejs/node/blob/master/lib/_http_server.js#L487), `this.writable === false` in [application.js](https://github.com/koajs/koa/blob/master/lib/application.js#L199)

Pull request description:
fix(response): correct response.writable logic
closes #779


Patch:
diff --git a/lib/response.js b/lib/response.js
index 9959747..e2038c0 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -478,16 +478,19 @@ module.exports = {
 
   /**
    * Checks if the request is writable.
-   * Tests for the existence of the socket
-   * as node sometimes does not set it.
    *
    * @return {Boolean}
    * @api private
    */
 
   get writable() {
+    // can't write any more after response finished
+    if (this.res.finished) return false;
+
     var socket = this.res.socket;
-    if (!socket) return false;
+    // There are already pending outgoing res, but still writable
+    // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
+    if (!socket) return true;
     return socket.writable;
   },
 


Test patch:
diff --git a/test/application.js b/test/application.js
index 0c1947c..d114f22 100644
--- a/test/application.js
+++ b/test/application.js
@@ -6,6 +6,7 @@ var request = require('supertest');
 var statuses = require('statuses');
 var assert = require('assert');
 var http = require('http');
+var net = require('net');
 var koa = require('..');
 var fs = require('fs');
 var AssertionError = assert.AssertionError;
diff --git a/test/response/writeable.js b/test/response/writeable.js
index ee4323b..88c3750 100644
--- a/test/response/writeable.js
+++ b/test/response/writeable.js
@@ -1,19 +1,105 @@
 
 'use strict';
 
-var response = require('../context').response;
+var should = require('should');
+var koa = require('../../');
+var net = require('net');
 
 describe('res.writable', function(){
-  it('should return the request is writable', function(){
-    var res = response();
-    res.writable.should.be.ok;
+  describe('when continuous requests in one persistent connection', function() {
+    function requestTwice(server, done) {
+      let port = server.address().port;
+      var buf = new Buffer('GET / HTTP/1.1\r\nHost: localhost:' + port + '\r\nConnection: keep-alive\r\n\r\n');
+      var client = net.connect(port);
+      var datas = [];
+      client
+        .on('error', done)
+        .on('data', function(data) {
+          datas.push(data);
+        })
+        .on('end', function() {
+          done(null, datas);
+        });
+      setImmediate(function() {
+        client.write(buf);
+      });
+      setImmediate(function() {
+        client.write(buf);
+      });
+      setTimeout(function() {
+        client.end();
+      }, 100);
+    }
+
+    it('should always writable and response all requests', function(done) {
+      var app = koa();
+      var count = 0;
+      app.use(function*() {
+        count++;
+        this.body = 'request ' + count + ', writable: ' + this.writable;
+      });
+
+      var server = app.listen();
+      requestTwice(server, function(err, datas) {
+        var responses = Buffer.concat(datas).toString();
+        responses.should.match(/request 1, writable: true/);
+        responses.should.match(/request 2, writable: true/);
+        done();
+      });
+    })
   })
 
-  describe('when res.socket not present', function (){
-    it('should return the request is not writable', function (){
-      var res = response();
-      res.res.socket = null;
-      res.writable.should.not.be.ok;
+  describe('when socket closed before response sent', function() {
+    function requsetClosed(server) {
+      let port = server.address().port;
+      var buf = new Buffer('GET / HTTP/1.1\r\nHost: localhost:' + port + '\r\nConnection: keep-alive\r\n\r\n');
+      var client = net.connect(port);
+      setImmediate(function() {
+        client.write(buf);
+        client.end();
+      });
+    }
+
+    it('should not writable', function(done) {
+      var app = koa();
+      app.use(function*() {
+        yield sleep(1000);
+        if (this.writable) return done(new Error('this.writable should not be true'));
+        done();
+      });
+      var server = app.listen();
+      requsetClosed(server);
+    })
+  })
+
+  describe('when resposne finished', function() {
+    function request(server) {
+      let port = server.address().port;
+      var buf = new Buffer('GET / HTTP/1.1\r\nHost: localhost:' + port + '\r\nConnection: keep-alive\r\n\r\n');
+      var client = net.connect(port);
+      setImmediate(function() {
+        client.write(buf);
+      });
+      setTimeout(function() {
+        client.end();
+      }, 100);
+    }
+
+    it('should not writable', function(done) {
+      var app = koa();
+      app.use(function*() {
+        this.res.end();
+        if (this.writable) return done(new Error('this.writable should not be true'));
+        done();
+      });
+      var server = app.listen();
+      request(server);
     })
   })
 })
+
+function sleep(time) {
+  return new Promise(function(resolve) {
+    setTimeout(resolve, time);
+  });
+}