Skip to content

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

@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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions