mirror of
https://github.com/hanzomsg/nats.js.git
synced 2026-10-30 20:31:36 +00:00
- Updated `MsgHdrs.fromRecord` to convert non-array header values to arrays. - Added tests to ensure proper normalization of such values. interesting use of building headers from the map outside of the internals. Signed-off-by: Alberto Ricart <[email protected]>
523 lines
13 KiB
TypeScript
523 lines
13 KiB
TypeScript
/*
|
|
* Copyright 2020-2023 The NATS Authors
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
import { connect } from "./connect.ts";
|
|
import {
|
|
canonicalMIMEHeaderKey,
|
|
createInbox,
|
|
Empty,
|
|
headers,
|
|
Match,
|
|
MsgHdrsImpl,
|
|
MsgImpl,
|
|
Parser,
|
|
} from "../src/internal_mod.ts";
|
|
import type {
|
|
NatsConnectionImpl,
|
|
Publisher,
|
|
RequestOptions,
|
|
} from "../src/internal_mod.ts";
|
|
import { NatsServer } from "../../test_helpers/launcher.ts";
|
|
import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert";
|
|
import { TestDispatcher } from "./parser_test.ts";
|
|
import { cleanup, setup } from "test_helpers";
|
|
import { errors } from "../src/errors.ts";
|
|
|
|
Deno.test("headers - illegal key", () => {
|
|
const h = headers();
|
|
["bad:", "bad ", String.fromCharCode(127)].forEach((v) => {
|
|
assertThrows(
|
|
() => {
|
|
h.set(v, "aaa");
|
|
},
|
|
errors.InvalidArgumentError,
|
|
"is not a valid character in a header name",
|
|
);
|
|
});
|
|
|
|
["\r", "\n"].forEach((v) => {
|
|
assertThrows(
|
|
() => {
|
|
h.set("a", v);
|
|
},
|
|
errors.InvalidArgumentError,
|
|
"values cannot contain \\r or \\n",
|
|
);
|
|
});
|
|
});
|
|
|
|
Deno.test("headers - case sensitive", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
h.set("a", "a");
|
|
assert(h.has("a"));
|
|
assert(!h.has("A"));
|
|
|
|
h.set("A", "A");
|
|
assert(h.has("A"));
|
|
|
|
assertEquals(h.size(), 2);
|
|
assertEquals(h.get("a"), "a");
|
|
assertEquals(h.values("a"), ["a"]);
|
|
assertEquals(h.get("A"), "A");
|
|
assertEquals(h.values("A"), ["A"]);
|
|
|
|
h.append("a", "aa");
|
|
h.append("A", "AA");
|
|
assertEquals(h.size(), 2);
|
|
assertEquals(h.values("a"), ["a", "aa"]);
|
|
assertEquals(h.values("A"), ["A", "AA"]);
|
|
|
|
h.delete("a");
|
|
assert(!h.has("a"));
|
|
assert(h.has("A"));
|
|
|
|
h.set("A", "AAA");
|
|
assertEquals(h.values("A"), ["AAA"]);
|
|
});
|
|
|
|
Deno.test("headers - case insensitive", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
h.set("a", "a", Match.IgnoreCase);
|
|
// set replaces
|
|
h.set("A", "A", Match.IgnoreCase);
|
|
assertEquals(h.size(), 1);
|
|
assert(h.has("a", Match.IgnoreCase));
|
|
assert(h.has("A", Match.IgnoreCase));
|
|
assertEquals(h.values("a", Match.IgnoreCase), ["A"]);
|
|
assertEquals(h.values("A", Match.IgnoreCase), ["A"]);
|
|
|
|
h.append("a", "aa");
|
|
assertEquals(h.size(), 2);
|
|
const v = h.values("a", Match.IgnoreCase);
|
|
v.sort();
|
|
assertEquals(v, ["A", "aa"]);
|
|
|
|
h.delete("a", Match.IgnoreCase);
|
|
assertEquals(h.size(), 0);
|
|
});
|
|
|
|
Deno.test("headers - mime", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
h.set("ab", "ab", Match.CanonicalMIME);
|
|
assert(!h.has("ab"));
|
|
assert(h.has("Ab"));
|
|
|
|
// set replaces
|
|
h.set("aB", "A", Match.CanonicalMIME);
|
|
assertEquals(h.size(), 1);
|
|
assert(h.has("Ab"));
|
|
|
|
h.append("ab", "aa", Match.CanonicalMIME);
|
|
assertEquals(h.size(), 1);
|
|
const v = h.values("ab", Match.CanonicalMIME);
|
|
v.sort();
|
|
assertEquals(v, ["A", "aa"]);
|
|
|
|
h.delete("ab", Match.CanonicalMIME);
|
|
assertEquals(h.size(), 0);
|
|
});
|
|
|
|
Deno.test("headers - publish has headers", async () => {
|
|
const srv = await NatsServer.start();
|
|
const nc = await connect(
|
|
{
|
|
port: srv.port,
|
|
},
|
|
);
|
|
|
|
const h = headers();
|
|
h.set("a", "aa");
|
|
h.set("b", "bb");
|
|
|
|
const subj = createInbox();
|
|
const sub = nc.subscribe(subj, { max: 1 });
|
|
const done = (async () => {
|
|
for await (const m of sub) {
|
|
assert(m.headers);
|
|
const mh = m.headers as MsgHdrsImpl;
|
|
assertEquals(mh.size(), 2);
|
|
assert(mh.has("a"));
|
|
assert(mh.has("b"));
|
|
}
|
|
})();
|
|
|
|
nc.publish(subj, Empty, { headers: h });
|
|
await done;
|
|
await nc.close();
|
|
await srv.stop();
|
|
});
|
|
|
|
Deno.test("headers - request has headers", async () => {
|
|
const srv = await NatsServer.start();
|
|
const nc = await connect({
|
|
port: srv.port,
|
|
});
|
|
const s = createInbox();
|
|
const sub = nc.subscribe(s, { max: 1 });
|
|
const done = (async () => {
|
|
for await (const m of sub) {
|
|
m.respond(Empty, { headers: m.headers });
|
|
}
|
|
})();
|
|
|
|
const opts = {} as RequestOptions;
|
|
opts.headers = headers();
|
|
opts.headers.set("x", "X");
|
|
const msg = await nc.request(s, Empty, opts);
|
|
assert(msg.headers);
|
|
const mh = msg.headers;
|
|
assert(mh.has("x"));
|
|
await done;
|
|
await nc.close();
|
|
await srv.stop();
|
|
});
|
|
|
|
function status(code: number, description: string): Uint8Array {
|
|
const status = code
|
|
? `NATS/1.0 ${code.toString()} ${description}`.trim()
|
|
: "NATS/1.0";
|
|
const line = `${status}\r\n\r\n\r\n`;
|
|
return new TextEncoder().encode(line);
|
|
}
|
|
|
|
function checkStatus(code = 200, description = "") {
|
|
const h = MsgHdrsImpl.decode(status(code, description));
|
|
const isErrorCode = code > 0 && (code < 200 || code >= 300);
|
|
assertEquals(h.hasError, isErrorCode);
|
|
|
|
if (code > 0) {
|
|
assertEquals(h.code, code);
|
|
assertEquals(h.description, description);
|
|
assertEquals(h.status, `${code} ${description}`.trim());
|
|
}
|
|
assertEquals(h.description, description);
|
|
}
|
|
|
|
Deno.test("headers - status", () => {
|
|
checkStatus(0, "");
|
|
checkStatus(200, "");
|
|
checkStatus(200, "OK");
|
|
checkStatus(503, "No Responders");
|
|
checkStatus(404, "No Messages");
|
|
});
|
|
|
|
Deno.test("headers - equality", () => {
|
|
const a = headers() as MsgHdrsImpl;
|
|
const b = headers() as MsgHdrsImpl;
|
|
assert(a.equals(b));
|
|
|
|
a.set("a", "b");
|
|
b.set("a", "b");
|
|
assert(a.equals(b));
|
|
|
|
b.append("a", "bb");
|
|
assert(!a.equals(b));
|
|
|
|
a.append("a", "cc");
|
|
assert(!a.equals(b));
|
|
});
|
|
|
|
Deno.test("headers - canonical", () => {
|
|
assertEquals(canonicalMIMEHeaderKey("foo"), "Foo");
|
|
assertEquals(canonicalMIMEHeaderKey("foo-bar"), "Foo-Bar");
|
|
assertEquals(canonicalMIMEHeaderKey("foo-bar-baz"), "Foo-Bar-Baz");
|
|
});
|
|
|
|
Deno.test("headers - append ignore case", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
h.set("a", "a");
|
|
h.append("A", "b", Match.IgnoreCase);
|
|
assertEquals(h.size(), 1);
|
|
assertEquals(h.values("a"), ["a", "b"]);
|
|
});
|
|
|
|
Deno.test("headers - append exact case", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
h.set("a", "a");
|
|
h.append("A", "b");
|
|
assertEquals(h.size(), 2);
|
|
assertEquals(h.values("a"), ["a"]);
|
|
assertEquals(h.values("A"), ["b"]);
|
|
});
|
|
|
|
Deno.test("headers - append canonical", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
h.set("a", "a");
|
|
h.append("A", "b", Match.CanonicalMIME);
|
|
assertEquals(h.size(), 2);
|
|
assertEquals(h.values("a"), ["a"]);
|
|
assertEquals(h.values("A"), ["b"]);
|
|
});
|
|
|
|
Deno.test("headers - malformed header are ignored", () => {
|
|
const te = new TextEncoder();
|
|
const d = new TestDispatcher();
|
|
const p = new Parser(d);
|
|
p.parse(
|
|
te.encode(`HMSG SUBJECT 1 REPLY 17 17\r\nNATS/1.0\r\nBAD\r\n\r\n\r\n`),
|
|
);
|
|
assertEquals(d.errs.length, 0);
|
|
assertEquals(d.msgs.length, 1);
|
|
const e = d.msgs[0];
|
|
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
|
|
const hi = m.headers as MsgHdrsImpl;
|
|
assertEquals(hi.size(), 0);
|
|
});
|
|
|
|
Deno.test("headers - handles no space", () => {
|
|
const te = new TextEncoder();
|
|
const d = new TestDispatcher();
|
|
const p = new Parser(d);
|
|
p.parse(
|
|
te.encode(`HMSG SUBJECT 1 REPLY 17 17\r\nNATS/1.0\r\nA:A\r\n\r\n\r\n`),
|
|
);
|
|
assertEquals(d.errs.length, 0);
|
|
assertEquals(d.msgs.length, 1);
|
|
const e = d.msgs[0];
|
|
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
|
|
assert(m.headers);
|
|
assertEquals(m.headers.get("A"), "A");
|
|
});
|
|
|
|
Deno.test("headers - trims values", () => {
|
|
const te = new TextEncoder();
|
|
const d = new TestDispatcher();
|
|
const p = new Parser(d);
|
|
p.parse(
|
|
te.encode(
|
|
`HMSG SUBJECT 1 REPLY 23 23\r\nNATS/1.0\r\nA: A \r\n\r\n\r\n`,
|
|
),
|
|
);
|
|
assertEquals(d.errs.length, 0);
|
|
assertEquals(d.msgs.length, 1);
|
|
const e = d.msgs[0];
|
|
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
|
|
assert(m.headers);
|
|
assertEquals(m.headers.get("A"), "A");
|
|
});
|
|
|
|
Deno.test("headers - error headers may have other entries", () => {
|
|
const te = new TextEncoder();
|
|
|
|
const d = new TestDispatcher();
|
|
const p = new Parser(d);
|
|
p.parse(
|
|
te.encode(
|
|
`HMSG _INBOX.DJ2IU18AMXPOZMG5R7NJVI 2 75 75\r\nNATS/1.0 100 Idle Heartbeat\r\nNats-Last-Consumer: 1\r\nNats-Last-Stream: 1\r\n\r\n\r\n`,
|
|
),
|
|
);
|
|
assertEquals(d.msgs.length, 1);
|
|
|
|
const e = d.msgs[0];
|
|
const m = new MsgImpl(e.msg!, e.data!, {} as Publisher);
|
|
assert(m.headers);
|
|
|
|
assertEquals(m.headers.get("Nats-Last-Consumer"), "1");
|
|
assertEquals(m.headers.get("Nats-Last-Stream"), "1");
|
|
});
|
|
|
|
Deno.test("headers - code/description", () => {
|
|
assertThrows(
|
|
() => {
|
|
headers(500);
|
|
},
|
|
Error,
|
|
"'description' is required",
|
|
);
|
|
|
|
assertThrows(
|
|
() => {
|
|
headers(0, "some message");
|
|
},
|
|
Error,
|
|
"'code' is required",
|
|
);
|
|
});
|
|
|
|
Deno.test("headers - codec", async () => {
|
|
const { ns, nc } = await setup({}, {});
|
|
|
|
nc.subscribe("foo", {
|
|
callback: (_err, msg) => {
|
|
const h = headers(500, "custom status from client");
|
|
msg.respond(Empty, { headers: h });
|
|
},
|
|
});
|
|
|
|
const r = await nc.request("foo", Empty);
|
|
assertEquals(r.headers?.code, 500);
|
|
assertEquals(r.headers?.description, "custom status from client");
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("headers - malformed headers", async () => {
|
|
const { ns, nc } = await setup();
|
|
const nci = nc as NatsConnectionImpl;
|
|
|
|
type t = {
|
|
proto: string;
|
|
expected: {
|
|
payload: string;
|
|
code?: number;
|
|
description?: string;
|
|
key?: string;
|
|
value?: string;
|
|
};
|
|
};
|
|
|
|
const h = headers(1, "h");
|
|
nc.publish("foo", Empty, { headers: h });
|
|
|
|
const tests: t[] = [
|
|
{
|
|
// extra spaces after subject, only new line after lengths
|
|
// trailing space after default status - this resulted in a
|
|
// NaN for the code but no crash
|
|
proto: "HPUB foo 13 15\nNATS/1.0 \r\n\r\nhi\r\n",
|
|
expected: {
|
|
payload: "hi",
|
|
code: 0,
|
|
description: "",
|
|
},
|
|
},
|
|
{
|
|
// extra spaces and pub lengths not followed by crlf
|
|
// status line followed by extra spaces etc
|
|
proto: "HPUB foo 17 19\nNATS/1.0 1 H\r\n\r\nhi\r\n",
|
|
expected: {
|
|
payload: "hi",
|
|
code: 1,
|
|
description: "H",
|
|
},
|
|
},
|
|
{
|
|
// server will convert this to msg, so no headers
|
|
proto: "HPUB foo 0 0\r\n\r\n",
|
|
expected: {
|
|
payload: "",
|
|
},
|
|
},
|
|
{
|
|
// this was the issue that broke java client
|
|
proto: "HPUB foo 12 12\r\nNATS/1.0\r\n\r\n\r\n",
|
|
expected: {
|
|
payload: "",
|
|
code: 0,
|
|
description: "",
|
|
},
|
|
},
|
|
];
|
|
|
|
const sub = nc.subscribe("foo", { max: tests.length });
|
|
let i = 0;
|
|
const done = (async () => {
|
|
for await (const m of sub) {
|
|
const t = tests[i++];
|
|
assertEquals(m.string(), t.expected.payload);
|
|
if (typeof t.expected.code === "number") {
|
|
assertEquals(m.headers?.code, t.expected.code);
|
|
assertEquals(m.headers?.description, t.expected.description);
|
|
if (t.expected.key) {
|
|
assertEquals(m.headers?.get(t.expected.key), t.expected.value);
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
|
|
tests.forEach((v) => {
|
|
nci.protocol.sendCommand(v.proto);
|
|
});
|
|
|
|
await done;
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("headers - iterator", () => {
|
|
const h = headers();
|
|
h.set("a", "aa");
|
|
h.set("b", "bb");
|
|
h.set("c", "cc");
|
|
|
|
let c = 0;
|
|
const abc = ["a", "b", "c"];
|
|
for (const tuple of h) {
|
|
const letter = abc[c];
|
|
assertEquals(tuple[0], letter);
|
|
assertEquals(tuple[1][0], `${letter}${letter}`);
|
|
c++;
|
|
}
|
|
assertEquals(c, 3);
|
|
});
|
|
|
|
Deno.test("headers - last", () => {
|
|
const h = headers();
|
|
h.set("a", "aa");
|
|
h.append("b", "bb");
|
|
h.append("b", "bbb");
|
|
|
|
assertEquals(h.last("foo"), "");
|
|
assertEquals(h.last("a"), "aa");
|
|
assertEquals(h.last("b"), "bbb");
|
|
});
|
|
|
|
Deno.test("headers - record", () => {
|
|
const h = headers();
|
|
h.set("a", "aa");
|
|
h.append("b", "bb");
|
|
h.append("b", "bbb");
|
|
|
|
const r = (h as MsgHdrsImpl).toRecord();
|
|
assertEquals(r, {
|
|
a: ["aa"],
|
|
b: ["bb", "bbb"],
|
|
});
|
|
|
|
const hr = MsgHdrsImpl.fromRecord(r) as MsgHdrsImpl;
|
|
assertEquals(hr.get("foo"), "");
|
|
assertEquals(h.get("a"), "aa");
|
|
assertEquals(h.get("b"), "bb");
|
|
assertEquals(h.last("b"), "bbb");
|
|
|
|
assert(hr.equals(h as MsgHdrsImpl));
|
|
});
|
|
|
|
Deno.test("headers - fromRecord normalizes non-array values", () => {
|
|
const hr = MsgHdrsImpl.fromRecord(
|
|
{ a: "x", d: 1 } as unknown as Record<string, string[]>,
|
|
) as MsgHdrsImpl;
|
|
assertEquals(hr.get("a"), "x");
|
|
assertEquals(hr.get("d"), "1");
|
|
|
|
const r = hr.toRecord();
|
|
assertEquals(r, {
|
|
a: ["x"],
|
|
d: ["1"],
|
|
});
|
|
});
|
|
|
|
Deno.test("headers - not equals", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
h.set("a", "aa");
|
|
h.append("b", "bb");
|
|
h.append("b", "bbb");
|
|
|
|
assertFalse(h.equals(headers() as MsgHdrsImpl));
|
|
});
|
|
|
|
Deno.test("headers - empty", () => {
|
|
const h = headers() as MsgHdrsImpl;
|
|
assertEquals(h.toString(), "");
|
|
});
|