Skip to content

http2 client drops response headers if the value ends with a space #56703

Closed
@timostamm

Description

@timostamm

Version

v23.6.1, v22.13.1, v20.18.2, v18.20.6

Platform

Darwin xxx 24.1.0 Darwin Kernel Version 24.1.0: Thu Oct 10 21:03:15 PDT 2024; root:xnu-11215.41.3~2/RELEASE_ARM64_T6000 arm64

Subsystem

http2

What steps will reproduce the bug?

Any response header value that starts or ends with a space or tab character is omitted in the HTTP/2 headers object of the response.

The server sends:

  res.writeHead(200, {
    "key1": "foo ",
    "key2": " foo",
    "key3": "foo\t",
    "key4": "\tfoo",
    "key5": "foo",
  })
;

The http2 client receives:

[Object: null prototype] {
  ':status': 200,
  key5: 'foo',
  date: 'Wed, 22 Jan 2025 13:03:04 GMT',
  [Symbol(nodejs.http2.sensitiveHeaders)]: []
}
Script to reproduce
import { createServer, connect } from "node:http2";

const port = 8011;

const server = createServer(async (req, res) => {
  res.writeHead(200, {
    "key1": "foo ",
    "key2": " foo",
    "key3": "foo\t",
    "key4": "\tfoo",
    "key5": "foo",
  });
  res.end();
});
server.listen(port, () => {
  // Alternatively, comment out call(), and run:
  // curl -v http://localhost:8011 --http2-prior-knowledge
  // This will show response headers with surrounding whitespace, proving that the issue is in the client.
  call();
});

function call() {
  const session = connect(`http://localhost:${port}`);
  session.on("error", (err) => console.error(err));
  const stream = session.request({
    ":path": "/",
  });
  stream.on("response", (headers, flags) => {
    console.log("client received response headers:", headers);
  });
  stream.on("end", () => {
    session.close();
    server.close();
  });
  stream.end();
}

How often does it reproduce? Is there a required condition?

Always, for response header values that start or end with a space or tab character.

What is the expected behavior? Why is that the expected behavior?

I expect to receive trimmed field values. I do not expect fields to be silently dropped.

Expected output:

[Object: null prototype] {
  ':status': 200,
  key1: 'foo',
  key2: 'foo',
  key3: 'foo',
  key4: 'foo',
  key5: 'foo',
  date: 'Wed, 22 Jan 2025 13:03:04 GMT',
  [Symbol(nodejs.http2.sensitiveHeaders)]: []
}

What do you see instead?

Omitted header fields key 1 to 4:

[Object: null prototype] {
  ':status': 200,
  key5: 'foo',
  date: 'Wed, 22 Jan 2025 13:03:04 GMT',
  [Symbol(nodejs.http2.sensitiveHeaders)]: []
}

Additional information

RFC 7540 states:

Any request or response that contains a character not permitted in a header field value MUST be treated as malformed (Section 8.1.2.6). Valid characters are defined by the "field-content" ABNF rule in Section 3.2 of [RFC7230].

RFC 7230 states:

Each header field consists of a case-insensitive field name followed by a colon (":"), optional leading whitespace, the field value, and optional trailing whitespace.

header-field   = field-name ":" OWS field-value OWS
[...]
field-value    = *( field-content / obs-fold )
field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]

Appendix B defines OWS = *( SP / HTAB ).

Based on this definition, I expect space and tab around field-content to be permitted in a header-field, but to be discarded when parsing the field-value.

Metadata

Metadata

Assignees

No one assigned

    Labels

    http2Issues or PRs related to the http2 subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions