Skip to content

HTTP binding: Content-Type matched as exact string, falsy data dropped, and ce- headers not percent-encoded #675

Open

Description

@trackoor

Describe the Bug

Three interoperability bugs in the HTTP protocol binding and JSON parser:

  1. Content-Type is matched as an exact string. parserByContentType (src/parsers.ts) only has literal keys ("application/json", "application/json; charset=utf-8", "application/cloudevents+json", "application/cloudevents+json; charset=utf-8", "application/octet-stream"), and getMode (src/message/http/index.ts) checks contentType.startsWith(...) case-sensitively. Structured messages with "application/cloudevents+json; charset=UTF-8" (the example in section 3.2.1 of the HTTP binding spec), "application/cloudevents+json;charset=utf-8", or "APPLICATION/CLOUDEVENTS+JSON" throw ValidationError. In binary mode, "application/json; charset=UTF-8", "application/json;charset=utf-8", "APPLICATION/JSON", and */*+json media types skip JSON parsing and leave event.data as the raw JSON string.
  2. Falsy data is dropped in structured mode, and JSON primitives become strings in binary mode. parseStructured copies parsed fields with if (property), so data: 0, false, "", and null become undefined (even when round-tripping HTTP.toEvent(HTTP.structured(ce))). In binary mode with content-type: application/json, JSONParser.parse wraps any string not starting with [, {, or " in quotes, turning JSON payloads 0, false, and null into "0", "false", and "null".
  3. Binary-mode ce- header values are neither percent-encoded nor percent-decoded. Section 3.1.3.2 of the HTTP binding requires percent-encoding space, ", %, and characters outside printable ASCII (U+0021-U+007E) when serializing ce- headers, and unescaping double quotes plus percent-decoding on receipt. HTTP.binary() writes raw strings, causing httpTransport to fail with ERR_INVALID_CHAR on non-ASCII attributes such as "Euro €", and HTTP.toEvent() leaves percent-encoded header values such as "Euro%20%E2%82%AC" undecoded.

Steps to Reproduce

  1. Install cloudevents@10.0.0 and save the script below as repro.mjs:
import http from "node:http";
import { CloudEvent, HTTP, emitterFor, httpTransport } from "cloudevents";

// 1. Content-Type media types and parameters matched as exact strings
const body = JSON.stringify({
  specversion: "1.0",
  id: "1",
  source: "/s",
  type: "t",
  data: { a: 1 },
});
for (const ct of [
  "application/cloudevents+json; charset=UTF-8",
  "application/cloudevents+json;charset=utf-8",
  "APPLICATION/CLOUDEVENTS+JSON",
]) {
  try {
    HTTP.toEvent({ headers: { "content-type": ct }, body });
  } catch (e) {
    console.log(ct, "->", e.message);
  }
}
const binJson = HTTP.toEvent({
  headers: {
    "ce-specversion": "1.0",
    "ce-id": "1",
    "ce-source": "/s",
    "ce-type": "t",
    "content-type": "application/json; charset=UTF-8",
  },
  body: "{\"a\":1}",
});
console.log("binary charset=UTF-8 data:", typeof binJson.data, binJson.data);

// 2. Falsy data dropped in structured mode; JSON primitives stringified in binary mode
for (const val of [0, false, "", null]) {
  const msg = HTTP.structured(new CloudEvent({ source: "/s", type: "t", data: val }));
  console.log("structured", JSON.stringify(val), "->", HTTP.toEvent(msg).data);
}
for (const raw of ["0", "false", "null"]) {
  const ev = HTTP.toEvent({
    headers: {
      "ce-specversion": "1.0",
      "ce-id": "1",
      "ce-source": "/s",
      "ce-type": "t",
      "content-type": "application/json",
    },
    body: raw,
  });
  console.log("binary", raw, "->", typeof ev.data, JSON.stringify(ev.data));
}

// 3. Binary-mode header values not percent-encoded or decoded
const out = HTTP.binary(new CloudEvent({ source: "/s", type: "t", subject: "a b%c\"€", data: "x" }));
console.log("encoded ce-subject:", out.headers["ce-subject"]);

const inn = HTTP.toEvent({
  headers: {
    "ce-specversion": "1.0",
    "ce-id": "1",
    "ce-source": "/s",
    "ce-type": "t",
    "ce-subject": "Euro%20%E2%82%AC",
    "content-type": "text/plain",
  },
  body: "x",
});
console.log("decoded subject:", inn.subject);

const srv = http.createServer((req, res) => res.end());
srv.listen(0, "127.0.0.1", async () => {
  const emit = emitterFor(httpTransport(`http://127.0.0.1:${srv.address().port}`));
  try {
    await emit(new CloudEvent({ source: "/s", type: "t", subject: "Euro €", data: "x" }));
  } catch (e) {
    console.log("httpTransport Euro € ->", e.code, e.message);
  }
  srv.close();
});
  1. Run node repro.mjs. Actual output:
application/cloudevents+json; charset=UTF-8 -> invalid content type application/cloudevents+json; charset=UTF-8
application/cloudevents+json;charset=utf-8 -> invalid content type application/cloudevents+json;charset=utf-8
APPLICATION/CLOUDEVENTS+JSON -> no cloud event detected
binary charset=UTF-8 data: string {"a":1}
structured 0 -> undefined
structured false -> undefined
structured "" -> undefined
structured null -> undefined
binary 0 -> string "0"
binary false -> string "false"
binary null -> string "null"
encoded ce-subject: a b%c"€
decoded subject: Euro%20%E2%82%AC
httpTransport Euro € -> ERR_INVALID_CHAR Invalid character in header content ["ce-subject"]

Expected Behavior

  1. Structured application/cloudevents+json headers parse regardless of media-type casing, optional whitespace around ;, or charset parameter casing, and binary */json or */*+json Content-Type headers parse JSON bodies into { a: 1 }.
  2. Structured events preserve data values 0, false, "", and null, and binary application/json bodies "0", "false", and "null" parse to 0, false, and null.
  3. HTTP.binary() percent-encodes ce-subject to "a%20b%25c%22%E2%82%AC" so httpTransport can send "Euro €" ("Euro%20%E2%82%AC"), and HTTP.toEvent() decodes "Euro%20%E2%82%AC" back to "Euro €".

Additional context

  • Versions tested: cloudevents@10.0.0 and main (3c5b538), on Node.js v26.13.0.
  • Relevant files: src/parsers.ts, src/message/http/index.ts, and src/message/http/headers.ts.

Credit: https://ucsbplse.github.io/

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions