Skip to content
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

No content length on DELETE and OPTIONS #27880

Open
ronag opened this issue May 26, 2019 · 26 comments
Open

No content length on DELETE and OPTIONS #27880

ronag opened this issue May 26, 2019 · 26 comments
Labels
confirmed-bug Issues with confirmed bugs. help wanted Issues that need assistance from volunteers or PRs that need help to proceed. http Issues or PRs related to the http subsystem.

Comments

@ronag
Copy link
Member

ronag commented May 26, 2019

Looking through http-party/node-http-proxy@8a24a1e and reading through the node code. I do believe we have an unhandled case.

I believe we are missing something here: https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js#L349

To ensure content-length and transfer-encoding are correct.

Am I on to something or should I drop this?

@addaleax
Copy link
Member

/cc @nodejs/http

@addaleax addaleax added the http Issues or PRs related to the http subsystem. label May 26, 2019
@awwright
Copy link
Contributor

awwright commented May 26, 2019

I don't think node-http-proxy is doing that correctly, methods do not affect the length of a request body in HTTP. (Some methods such as TRACE prohibit request bodies, however, these are considered client errors by the origin server, they are otherwise perfectly parsable messages.) According to HTTP/1.1 the message body length is dependent on:

  • The HTTP version;
  • if the message is a request or response;
  • for the response, the request method and/or response status;
  • and several headers that indicate length (Transfer-Encoding and Content-Length).

[edit] See discussion below, it turns out Node.js has behavior inconsistent with HTTP/1.1 messaging.

@lpinca
Copy link
Member

lpinca commented May 27, 2019

This is done on purpose, see

@awwright
Copy link
Contributor

awwright commented May 27, 2019

Oh, I didn't realize Node.js did this.

Setting per-method defaults on what transfer method to use seems like the wrong way to go about this. POST can use or prohibit any transfer method - one endpoint might prohibit a request body and chunked encoding, another might require chunked encoding.

Setting transfer codings (or lack thereof) based on request method isn't specified by HTTP and doesn't solve the general issue.

@awwright
Copy link
Contributor

I wrote a little script to determine and log how requests are being made:

// httpd
const http = require('http');
http.createServer(function(req, res){
    console.log(req.url);
    res.setHeader('Content-Type', 'text/plain');
    for(var i=0; i<req.rawHeaders.length; i+=2){
        res.write('' + req.rawHeaders[i]+': '+req.rawHeaders[i+1]+'\r\n');
    }
    res.end();
    return;
}).listen(8081, '0.0.0.0');
var http = require('http');
var expectedHeaders = {
  'DELETE': ['host', 'connection'],
  'GET': ['host', 'connection'],
  'HEAD': ['host', 'connection'],
  'POST': ['host', 'connection', 'transfer-encoding'],
  'PUT': ['host', 'connection', 'transfer-encoding']
};
var expectedMethods = Object.keys(expectedHeaders);
var i=0;
function next(){
    method = expectedMethods[i++];
    if(!method) return;
    console.log('');
    console.log(method);
    var req = http.request({
        method: method,
        port: 8081,
    }, function(res){ res.pipe(process.stdout); res.on('end', next); res.on('error', console.error) });
    // req.removeHeader('Content-Length');
    // req.setHeader('Transfer-Encoding', 'chunked');
    // req.setHeader('Connection', 'Transfer-Encoding');
    req.write('1');
    req.end();
    req.on('error', console.error);
}
next();

I can use curl to make DELETE requests with a body.

But to get Node.js to, I have to explicitly set req.setHeader('Transfer-Encoding', 'chunked');, otherwise it emits an invalid HTTP request, it emits a request without a Transfer-Encoding, without a Content-Length, but writes three bytes to the request body anyways. The server tries to parse this as the start of the next message, and it closes the connection.

This seems like a Node.js bug after all.

@awwright
Copy link
Contributor

This is... something. If that DELETE request with a phantom body looks like an HTTP request (e.g. req.write('PUT / HTTP/1.1\r\n\r\n');), the downstream server won't close the socket, which confirms that Node.js is sending as HTTP headers what is being accepted as message data.

This opens the potential for a carefully crafted message to bypass limits on the forwarded HTTP message.

On top of that, it's strange that the Node.js server is accepting this phantom request, even though the first request specified Connection: close which should cause the Node.js server to ignore additional requests.

Is Node.js doing anything to handle the Connection: close header?

@lpinca
Copy link
Member

lpinca commented May 27, 2019

the downstream server won't close the socket, which confirms that Node.js is sending as HTTP headers what is being accepted as message data.

I don't understand can you please clarify? You are writing request headers in the response body in the above example. What's wrong with this

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

const data = 'PUT / HTTP/1.1\r\n\r\n';

const server = http.createServer(function(req, res) {
  req.on('data', function(chunk) {
    assert.strictEqual(chunk, Buffer.from(data))
  });
  res.setHeader('Content-Type', 'text/plain');
  for (let i = 0; i < req.rawHeaders.length; i += 2) {
    res.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`);
  }
  res.end();
});

server.listen(8080, function() {
  const req = http.request({ method: 'DELETE', port: 8080 }, function(res) {
    console.log(res.headers);

    const chunks = [];
    res.on('data', function(chunk) {
      chunks.push(chunk);
    });

    res.on('end', function() {
      assert.deepStrictEqual(
        Buffer.concat(chunks),
        Buffer.from('Host: localhost:8080\r\nConnection: close\r\n')
      );
    });
  });

  req.write(data);
  req.end();
});

@ronag
Copy link
Member Author

ronag commented May 27, 2019

@awwright referenced this useful document in the other issue, https://httpwg.org/specs/rfc7230.html#message.body.length

@awwright
Copy link
Contributor

awwright commented May 27, 2019 via email

@lpinca
Copy link
Member

lpinca commented May 27, 2019

No, what Node.js actually writes appears to the server to be a second request!

Oh I get it now.

@ronag
Copy link
Member Author

ronag commented Jul 18, 2019

@lpinca do we do anything here or close?

@lpinca
Copy link
Member

lpinca commented Jul 18, 2019

The issue reported by @awwright is bad and should be fixed. We can close this but only because discussion is confusing here. If we close we should open a new issue only for #27880 (comment) and #27880 (comment).

@ronag
Copy link
Member Author

ronag commented Jul 19, 2019

@lpinca I don't actually understand any of this... @awwright would you mind extracting your comments into an easier to follow issue?

@lpinca
Copy link
Member

lpinca commented Jul 19, 2019

@ronag basically with a single http.request() it is possible to trigger multiple 'request' events on the server.

@awwright
Copy link
Contributor

@ronag I'll see if I can write up a clearer issue soon, but the write() behavior is really just a side effect of this issue; that Node.js is setting different default headers for different methods, when it should actually be based on the message payload. Fixing req.write() wouldn't solve the underlying issue.

@ronag
Copy link
Member Author

ronag commented Jul 21, 2019

@awwright: that’s my point. I think you have a fundamentally better understanding. I think you should describe what you think is the problem and best solution.

@ronag
Copy link
Member Author

ronag commented Jan 3, 2020

@awwright: ping?

@awwright
Copy link
Contributor

awwright commented Jan 3, 2020

Uh yes, it's been a while since I've run into this, but I'll try to think more about it here

@jasnell
Copy link
Member

jasnell commented Jun 26, 2020

Given that it's not clear if there's anything remaining that is actionable here, closing.

@awwright
Copy link
Contributor

@jasnell This is actionable, I just threw together some tests for the behavior here, see #34066

@ronag ronag reopened this Jun 26, 2020
@ronag ronag added the confirmed-bug Issues with confirmed bugs. label Apr 25, 2021
@ronag
Copy link
Member Author

ronag commented Apr 25, 2021

Given #34066 I believe we can consider this as a confirmed bug.

@ronag ronag added the help wanted Issues that need assistance from volunteers or PRs that need help to proceed. label Apr 25, 2021
@JayHelton
Copy link

Is this available for contribution? I would like to look into this.

@lpinca
Copy link
Member

lpinca commented Jul 20, 2021

@JayHelton sure.

@98lenvi
Copy link
Contributor

98lenvi commented Sep 26, 2022

I'm interested in working on this. I see two action points from the above conversation

  • Finish up the smaller changes in this test diff
  • Come up with a proposal on how we are going to set the headers like Content-Length, Transfer-Encoding. Based on the above conversation, I think this RFC has all the answers.

@brc-dd
Copy link

brc-dd commented Oct 31, 2023

Fixed via nodejs/undici#2305 I believe?

@awwright
Copy link
Contributor

awwright commented Nov 1, 2023

If this is fixed then #34066 should pass and be merged in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug Issues with confirmed bugs. help wanted Issues that need assistance from volunteers or PRs that need help to proceed. http Issues or PRs related to the http subsystem.
Projects
None yet
Development

No branches or pull requests

8 participants