Open
Description
Describe the Bug
Three interoperability bugs in the HTTP protocol binding and JSON parser:
Content-Typeis 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"), andgetMode(src/message/http/index.ts) checkscontentType.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"throwValidationError. In binary mode,"application/json; charset=UTF-8","application/json;charset=utf-8","APPLICATION/JSON", and*/*+jsonmedia types skip JSON parsing and leaveevent.dataas the raw JSON string.- Falsy
datais dropped in structured mode, and JSON primitives become strings in binary mode.parseStructuredcopies parsed fields withif (property), sodata: 0,false,"", andnullbecomeundefined(even when round-trippingHTTP.toEvent(HTTP.structured(ce))). In binary mode withcontent-type: application/json,JSONParser.parsewraps any string not starting with[,{, or"in quotes, turning JSON payloads0,false, andnullinto"0","false", and"null". - 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 serializingce-headers, and unescaping double quotes plus percent-decoding on receipt.HTTP.binary()writes raw strings, causinghttpTransportto fail withERR_INVALID_CHARon non-ASCII attributes such as"Euro €", andHTTP.toEvent()leaves percent-encoded header values such as"Euro%20%E2%82%AC"undecoded.
Steps to Reproduce
- Install
cloudevents@10.0.0and save the script below asrepro.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();
});- 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
- Structured
application/cloudevents+jsonheaders parse regardless of media-type casing, optional whitespace around;, orcharsetparameter casing, and binary*/jsonor*/*+jsonContent-Typeheaders parse JSON bodies into{ a: 1 }. - Structured events preserve
datavalues0,false,"", andnull, and binaryapplication/jsonbodies"0","false", and"null"parse to0,false, andnull. HTTP.binary()percent-encodesce-subjectto"a%20b%25c%22%E2%82%AC"sohttpTransportcan send"Euro €"("Euro%20%E2%82%AC"), andHTTP.toEvent()decodes"Euro%20%E2%82%AC"back to"Euro €".
Additional context
- Versions tested:
cloudevents@10.0.0andmain(3c5b538), on Node.jsv26.13.0. - Relevant files:
src/parsers.ts,src/message/http/index.ts, andsrc/message/http/headers.ts.
Credit: https://ucsbplse.github.io/
Metadata
Metadata
Assignees
Labels
No labels
Activity