diff --git a/nats-base-client/headers.ts b/nats-base-client/headers.ts index feeaa834..9aa6b9d2 100644 --- a/nats-base-client/headers.ts +++ b/nats-base-client/headers.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 The NATS Authors + * Copyright 2020-2022 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 @@ -77,8 +77,11 @@ export function canonicalMIMEHeaderKey(k: string): string { return String.fromCharCode(...buf); } -export function headers(): MsgHdrs { - return new MsgHdrsImpl(); +export function headers(code = 0, description = ""): MsgHdrs { + if ((code === 0 && description !== "") || (code > 0 && description === "")) { + throw new Error("setting status requires both code and description"); + } + return new MsgHdrsImpl(code, description); } const HEADER = "NATS/1.0"; @@ -93,14 +96,14 @@ export enum Match { } export class MsgHdrsImpl implements MsgHdrs { - code: number; + _code: number; headers: Map; - description: string; + _description: string; - constructor() { - this.code = 0; + constructor(code = 0, description = "") { + this._code = code; + this._description = description; this.headers = new Map(); - this.description = ""; } [Symbol.iterator]() { @@ -114,7 +117,7 @@ export class MsgHdrsImpl implements MsgHdrs { equals(mh: MsgHdrsImpl): boolean { if ( mh && this.headers.size === mh.headers.size && - this.code === mh.code + this._code === mh._code ) { for (const [k, v] of this.headers) { const a = mh.values(k); @@ -141,10 +144,10 @@ export class MsgHdrsImpl implements MsgHdrs { const h = lines[0]; if (h !== HEADER) { let str = h.replace(HEADER, ""); - mh.code = parseInt(str, 10); - const scode = mh.code.toString(); + mh._code = parseInt(str, 10); + const scode = mh._code.toString(); str = str.replace(scode, ""); - mh.description = str.trim(); + mh._description = str.trim(); } if (lines.length >= 1) { lines.slice(1).map((s) => { @@ -162,10 +165,13 @@ export class MsgHdrsImpl implements MsgHdrs { } toString(): string { - if (this.headers.size === 0) { + if (this.headers.size === 0 && this._code === 0) { return ""; } let s = HEADER; + if (this._code > 0 && this._description !== "") { + s += ` ${this._code} ${this._description}`; + } for (const [k, v] of this.headers) { for (let i = 0; i < v.length; i++) { s = `${s}\r\n${k}: ${v[i]}`; @@ -278,11 +284,11 @@ export class MsgHdrsImpl implements MsgHdrs { } get hasError() { - return this.code >= 300; + return this._code >= 300; } get status(): string { - return `${this.code} ${this.description}`.trim(); + return `${this._code} ${this._description}`.trim(); } toRecord(): Record { @@ -293,6 +299,14 @@ export class MsgHdrsImpl implements MsgHdrs { return data; } + get code(): number { + return this._code; + } + + get description(): string { + return this._description; + } + static fromRecord(r: Record): MsgHdrs { const h = new MsgHdrsImpl(); for (const k in r) { diff --git a/nats-base-client/jsutil.ts b/nats-base-client/jsutil.ts index 20a5dd65..da77de10 100644 --- a/nats-base-client/jsutil.ts +++ b/nats-base-client/jsutil.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The NATS Authors + * Copyright 2021-2022 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 @@ -116,9 +116,7 @@ export function newJsErrorMsg( description: string, subject: string, ): Msg { - const h = headers() as MsgHdrsImpl; - h.code = code; - h.description = description; + const h = headers(code, description) as MsgHdrsImpl; const arg = { hdr: 1, sid: 0, size: 0 } as MsgArg; const msg = new MsgImpl(arg, Empty, {} as Publisher); diff --git a/nats-base-client/msg.ts b/nats-base-client/msg.ts index e8ba62b8..e07a9f76 100644 --- a/nats-base-client/msg.ts +++ b/nats-base-client/msg.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The NATS Authors + * Copyright 2020-2022 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 @@ -20,15 +20,11 @@ import { TD } from "./encoders.ts"; import { ErrorCode, NatsError } from "./error.ts"; export function isRequestError(msg: Msg): NatsError | null { - // to consider an error from the server we expect no payload - if (msg && msg.data.length === 0 && msg.headers) { - const headers = msg.headers as MsgHdrsImpl; - if (headers.hasError) { - // only 503s are expected from core NATS (404/408/409s are JetStream) - if (headers.code === 503) { - return NatsError.errorForCode(ErrorCode.NoResponders); - } - } + // NATS core only considers errors 503s on messages that have no payload + // everything else simply forwarded as part of the message and is considered + // application level information + if (msg && msg.data.length === 0 && msg.headers?.code === 503) { + return NatsError.errorForCode(ErrorCode.NoResponders); } return null; } diff --git a/tests/headers_test.ts b/tests/headers_test.ts index bcee502c..80b7cf2a 100644 --- a/tests/headers_test.ts +++ b/tests/headers_test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 The NATS Authors + * Copyright 2020-2022 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 @@ -28,6 +28,7 @@ import { NatsServer } from "./helpers/launcher.ts"; import { assert, assertEquals, + assertRejects, assertThrows, } from "https://deno.land/std@0.152.0/testing/asserts.ts"; import { @@ -37,6 +38,7 @@ import { } from "../nats-base-client/internal_mod.ts"; import { Publisher } from "../nats-base-client/protocol.ts"; import { TestDispatcher } from "./parser_test.ts"; +import { cleanup, setup } from "./jstest_util.ts"; Deno.test("headers - illegal key", () => { const h = headers(); @@ -321,3 +323,38 @@ Deno.test("headers - error headers may have other entries", () => { assertEquals(m.headers.get(JsHeaders.LastConsumerSeqHdr), "1"); assertEquals(m.headers.get(JsHeaders.LastStreamSeqHdr), "1"); }); + +Deno.test("headers - code/description", () => { + assertThrows( + () => { + headers(500); + }, + Error, + "setting status requires both code and description", + ); + + assertThrows( + () => { + headers(0, "some message"); + }, + Error, + "setting status requires both code and description", + ); +}); + +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); +});