Skip to content

Empty body methods are inconsistent between H1 and H2 for QUERY, PROPFIND, and PROPPATCH #5171

@trivikr

Description

@trivikr

Bug Description

Undici handles empty request bodies differently between HTTP/1.1 and HTTP/2 for methods that can carry payloads.

Reproducible By

import { createServer } from "node:http";
import { createSecureServer } from "node:http2";
import { once } from "node:events";
import { Client } from "undici";
import pem from "@metcoder95/https-pem";

const methods = ["QUERY", "PROPFIND", "PROPPATCH"];

async function collectBody(body) {
  for await (const _ of body) {
    // Drain the response body so the request completes.
  }
}

async function requestH1(method) {
  let receivedContentLength;

  const server = createServer((req, res) => {
    receivedContentLength = req.headers["content-length"];
    req.resume();
    res.end("ok");
  });

  await once(server.listen(0), "listening");

  const client = new Client(`http://localhost:${server.address().port}`);

  try {
    const response = await client.request({ path: "/", method, body: "" });
    await collectBody(response.body);
    return receivedContentLength;
  } finally {
    await client.close();
    server.close();
    await once(server, "close");
  }
}

async function requestH2(method) {
  let receivedContentLength;

  const server = createSecureServer(
    await pem.generate({ opts: { keySize: 2048 } }),
  );

  server.on("stream", (stream, headers) => {
    receivedContentLength = headers["content-length"];
    stream.respond({ ":status": 200 });
    stream.end("ok");
  });

  await once(server.listen(0), "listening");

  const client = new Client(`https://localhost:${server.address().port}`, {
    connect: { rejectUnauthorized: false },
  });

  try {
    const response = await client.request({ path: "/", method, body: "" });
    await collectBody(response.body);
    return receivedContentLength;
  } finally {
    await client.close();
    server.close();
    await once(server, "close");
  }
}

for (const method of methods) {
  const h1ContentLength = await requestH1(method);
  const h2ContentLength = await requestH2(method);

  console.log(`${method}:`);
  console.log(`  H1 content-length: ${h1ContentLength}`);
  console.log(`  H2 content-length: ${h2ContentLength}`);
}

Expected Behavior

HTTP/1.1 and HTTP/2 should be consistent for empty payload-capable methods.

QUERY, PROPFIND, and PROPPATCH should be included in H2 expectsPayload, matching H1 behavior, so empty requests send content-length: 0 consistently.

Logs & Screenshots

QUERY:
  H1 content-length: 0
  H2 content-length: undefined
PROPFIND:
  H1 content-length: 0
  H2 content-length: undefined
PROPPATCH:
  H1 content-length: 0
  H2 content-length: undefined

Environment

macOS 26.4.1
Node v24.15.0
undici v8.2.0

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

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