Skip to content

bug report! If koa and http2server are used together, the response with method "head" request will never end! #1547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
masx200 opened this issue Jun 17, 2021 · 12 comments
Milestone

Comments

@masx200
Copy link

masx200 commented Jun 17, 2021

bug report! If koa and http2server are used together, the response with method "head" request will never end!

import http2 from "http2";
import Koa from "koa";
import fs from "fs";
import { fileURLToPath } from "url";
import { join, dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = new Koa();
const selfSignedKey = join(__dirname, "./certs/self-signed.key.pem");
const selfSignedCert = join(__dirname, "./certs/self-signed.cert.pem");
app.use((ctx) => {
    ctx.body = "Hello Koa";
});
const config = {
    sslKey: fs.readFileSync(selfSignedKey).toString(),
    sslCert: fs.readFileSync(selfSignedCert).toString(),
};
const serverHandler = app.callback();
const httpsopt = { cert: config.sslCert, key: config.sslKey };
const server = http2.createSecureServer(httpsopt, serverHandler);

server.on("listening", () => {
    console.log(`Server listening on ` + JSON.stringify(server.address()));
});
server.listen(3000);
{
    "type": "module",
    "dependencies": {
        "koa": "^2.13.1"
    }
}
node index.js

Initiate a "head" request in the browser.

Request URL: https://localhost:3000/
Referrer Policy: strict-origin-when-cross-origin
:authority: localhost:3000
:method: HEAD
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
referer: https://localhost:3000/
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-origin
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36

the response with method "head" request will never end!

@masx200
Copy link
Author

masx200 commented Jun 17, 2021

My solution is to modify it as follows and everything works fine!

import http2 from "http2";
import Koa from "koa";
import fs from "fs";
import { fileURLToPath } from "url";
import { join, dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = new Koa();
const selfSignedKey = join(__dirname, "./certs/self-signed.key.pem");
const selfSignedCert = join(__dirname, "./certs/self-signed.cert.pem");

//
app.use(async (ctx, next) => {
    await next();
    if (ctx.method === "HEAD") {
        ctx.res.end();
    }
    return;
});
//
app.use((ctx) => {
    ctx.body = "Hello Koa";
});
const config = {
    sslKey: fs.readFileSync(selfSignedKey).toString(),
    sslCert: fs.readFileSync(selfSignedCert).toString(),
};
const serverHandler = app.callback();
const httpsopt = { cert: config.sslCert, key: config.sslKey };
const server = http2.createSecureServer(httpsopt, serverHandler);

server.on("listening", () => {
    console.log(`Server listening on ` + JSON.stringify(server.address()));
});
server.listen(3000);

@masx200
Copy link
Author

masx200 commented Jun 24, 2021

return socket.writable;

I found the problem,

if it is the'http2' server and the method is 'head', the "socket.writable" is "false".

If it is the'http1' server, the "socket.writable" is "true".

@masx200
Copy link
Author

masx200 commented Jun 24, 2021

#1548

@Azlond
Copy link
Contributor

Azlond commented Aug 30, 2021

The same error also occurs when the client closes the connection before the server had a chance to respond, causing the socket to no longer be writable. This can cause pending connections, because ctx.res.end(); will never be called.

All other return-statements in the respond()-Function are calling res.end() (except for the koa-bypass), which makes me believe it should be called in that line as well - we have fixed it this way in our fork and haven't had any problems with pending connections since.

Instead of checking for

ctx.method === "HEAD"

as was done in #1548 I'd suggest replacing this line with the following:

if (!ctx.writable) {
    return res.end();
}

Would you like a separate Pull request for this @jonathanong ?

@masx200
Copy link
Author

masx200 commented May 19, 2023

Why has this problem not been solved after two years?

@jonathanong jonathanong added this to the v3.0.0 milestone Aug 29, 2024
@koajs koajs deleted a comment from Smem1234 Oct 20, 2024
@koajs koajs deleted a comment from Smem1234 Oct 20, 2024
@koajs koajs deleted a comment from Smem1234 Oct 20, 2024
titanism added a commit that referenced this issue Feb 27, 2025
… 1547) (#1593)

Relates to issue #1547, see my [comment
there](#1547 (comment))

Co-authored-by: Jan Kaiser <[email protected]>
Co-authored-by: titanism <[email protected]>
@titanism
Copy link
Contributor

I've merged #1593 (399cb6b).

@titanism titanism mentioned this issue Feb 27, 2025
13 tasks
@titanism
Copy link
Contributor

v2.16.0 released to npm with the backported fix

https://github.com/koajs/koa/releases/tag/2.16.0
https://www.npmjs.com/package/koa/v/2.16.0

We needed this for our work on @forwardemail (since we use HTTP2 server)

@Azlond
Copy link
Contributor

Azlond commented Apr 15, 2025

@titanism there's a regression in 2.16.1 - the fix is no longer included

Image.

I'm also not seeing my fix/commit in the v2.x branch - where did you backport it to?

Could you reopen the issue until this is fixed again?

@titanism
Copy link
Contributor

399cb6b

@titanism titanism reopened this Apr 15, 2025
@titanism
Copy link
Contributor

@jonathanong @fengmk2 can you please add this 399cb6b to 2.16.2? 2.16.1 is missing, but 2.16.0 has it

@titanism
Copy link
Contributor

The code change in 2.16.0 needs to be added to the regression in 2.16.1, and a new version 2.16.2 needs published.

https://www.npmjs.com/package/koa/v/2.16.0?activeTab=code → go to code view for 2.16.0 and look at lib/application.js file

/**
 * Response helper.
 */

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  const res = ctx.res;

  if (!ctx.writable) return res.end();

  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type');
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' === typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

Ref: #1547
Ref: #1548

The version of this function in 2.16.1 differs.

@jonathanong
Copy link
Member

@titanism what happened? how was it not on the 2.x branch?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants