• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nats-io / nats.js / 13041776921

29 Jan 2025 10:36PM UTC coverage: 82.727% (+0.02%) from 82.71%
13041776921

push

github

web-flow
fix: typo in the documentation of the list() method of the Objm class (#198)

2271 of 3098 branches covered (73.31%)

Branch coverage included in aggregate %.

9631 of 11289 relevant lines covered (85.31%)

787245.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.49
/core/src/headers.ts
1
/*
2
 * Copyright 2020-2023 The NATS Authors
3
 * Licensed under the Apache License, Version 2.0 (the "License");
4
 * you may not use this file except in compliance with the License.
5
 * You may obtain a copy of the License at
6
 *
7
 * http://www.apache.org/licenses/LICENSE-2.0
8
 *
9
 * Unless required by applicable law or agreed to in writing, software
10
 * distributed under the License is distributed on an "AS IS" BASIS,
11
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
 * See the License for the specific language governing permissions and
13
 * limitations under the License.
14
 */
15

16
// Heavily inspired by Golang's https://golang.org/src/net/http/header.go
17

18
import { TD, TE } from "./encoders.ts";
55✔
19
import type { MsgHdrs } from "./core.ts";
20
import { Match } from "./core.ts";
55✔
21
import { InvalidArgumentError } from "./errors.ts";
55✔
22

23
// https://www.ietf.org/rfc/rfc822.txt
24
// 3.1.2.  STRUCTURE OF HEADER FIELDS
25
//
26
// Once a field has been unfolded, it may be viewed as being com-
27
// posed of a field-name followed by a colon (":"), followed by a
28
// field-body, and  terminated  by  a  carriage-return/line-feed.
29
// The  field-name must be composed of printable ASCII characters
30
// (i.e., characters that  have  values  between  33.  and  126.,
31
// decimal, except colon).  The field-body may be composed of any
32
// ASCII characters, except CR or LF.  (While CR and/or LF may be
33
// present  in the actual text, they are removed by the action of
34
// unfolding the field.)
35
export function canonicalMIMEHeaderKey(k: string): string {
54✔
36
  const a = 97;
1,352✔
37
  const A = 65;
1,352✔
38
  const Z = 90;
1,352✔
39
  const z = 122;
1,352✔
40
  const dash = 45;
1,352✔
41
  const colon = 58;
1,352✔
42
  const start = 33;
1,352✔
43
  const end = 126;
1,352✔
44
  const toLower = a - A;
1,352✔
45

46
  let upper = true;
1,352✔
47
  const buf: number[] = new Array(k.length);
1,352✔
48
  for (let i = 0; i < k.length; i++) {
1,352✔
49
    let c = k.charCodeAt(i);
19,779✔
50
    if (c === colon || c < start || c > end) {
5,507✔
51
      throw InvalidArgumentError.format(
5,510✔
52
        "header",
5,510✔
53
        `'${k[i]}' is not a valid character in a header name`,
5,510✔
54
      );
55
    }
5,510✔
56
    if (upper && a <= c && c <= z) {
13,521✔
57
      c -= toLower;
13,987✔
58
    } else if (!upper && A <= c && c <= Z) {
5,507✔
59
      c += toLower;
16,107✔
60
    }
16,107✔
61
    buf[i] = c;
24,778✔
62
    upper = c == dash;
24,778✔
63
  }
24,778✔
64
  return String.fromCharCode(...buf);
1,821✔
65
}
1,352✔
66

67
export function headers(code = 0, description = ""): MsgHdrs {
54✔
68
  if ((code === 0 && description !== "") || (code > 0 && description === "")) {
261✔
69
    throw InvalidArgumentError.format("description", "is required");
263✔
70
  }
263✔
71
  return new MsgHdrsImpl(code, description);
120,450✔
72
}
120,224✔
73

74
const HEADER = "NATS/1.0";
55✔
75

76
export class MsgHdrsImpl implements MsgHdrs {
55✔
77
  _code: number;
54✔
78
  headers: Map<string, string[]>;
120,765✔
79
  _description: string;
54✔
80

81
  constructor(code = 0, description = "") {
54✔
82
    this._code = code;
120,765✔
83
    this._description = description;
120,765✔
84
    this.headers = new Map();
120,765✔
85
  }
120,765✔
86

87
  [Symbol.iterator](): IterableIterator<[string, string[]]> {
88✔
88
    return this.headers.entries();
34✔
89
  }
34✔
90

91
  size(): number {
33✔
92
    return this.headers.size;
46✔
93
  }
46✔
94

95
  equals(mh: MsgHdrsImpl): boolean {
33✔
96
    if (
41✔
97
      mh && this.headers.size === mh.headers.size &&
41✔
98
      this._code === mh._code
41✔
99
    ) {
41✔
100
      for (const [k, v] of this.headers) {
48✔
101
        const a = mh.values(k);
48✔
102
        if (v.length !== a.length) {
48✔
103
          return false;
49✔
104
        }
49✔
105
        const vv = [...v].sort();
162✔
106
        const aa = [...a].sort();
162✔
107
        for (let i = 0; i < vv.length; i++) {
48✔
108
          if (vv[i] !== aa[i]) {
56✔
109
            return false;
57✔
110
          }
57✔
111
        }
56✔
112
      }
53✔
113
      return true;
53✔
114
    }
53✔
115
    return false;
42✔
116
  }
41✔
117

118
  static decode(a: Uint8Array): MsgHdrsImpl {
53✔
119
    const mh = new MsgHdrsImpl();
585✔
120
    const s = TD.decode(a);
585✔
121
    const lines = s.split("\r\n");
585✔
122
    const h = lines[0];
585✔
123
    if (h !== HEADER) {
585✔
124
      // malformed headers could add extra space without adding a code or description
125
      let str = h.replace(HEADER, "").trim();
784✔
126
      if (str.length > 0) {
784✔
127
        mh._code = parseInt(str, 10);
801✔
128
        if (isNaN(mh._code)) {
×
129
          mh._code = 0;
×
130
        }
×
131
        const scode = mh._code.toString();
801✔
132
        str = str.replace(scode, "");
801✔
133
        mh._description = str.trim();
801✔
134
      }
801✔
135
    }
784✔
136
    if (lines.length >= 1) {
585✔
137
      lines.slice(1).map((s) => {
585✔
138
        if (s) {
2,492✔
139
          const idx = s.indexOf(":");
3,330✔
140
          if (idx > -1) {
3,330✔
141
            const k = s.slice(0, idx);
3,543✔
142
            const v = s.slice(idx + 1).trim();
3,543✔
143
            mh.append(k, v);
3,543✔
144
          }
3,543✔
145
        }
3,330✔
146
      });
585✔
147
    }
585✔
148
    return mh;
585✔
149
  }
585✔
150

151
  toString(): string {
54✔
152
    if (this.headers.size === 0 && this._code === 0) {
120,211✔
153
      return "";
239,982✔
154
    }
239,982✔
155
    let s = HEADER;
223,146✔
156
    if (this._code > 0 && this._description !== "") {
246!
157
      s += ` ${this._code} ${this._description}`;
248✔
158
    }
248✔
159
    for (const [k, v] of this.headers) {
223,017✔
160
      for (let i = 0; i < v.length; i++) {
223,427✔
161
        s = `${s}\r\n${k}: ${v[i]}`;
223,427✔
162
      }
223,427✔
163
    }
223,427✔
164
    return `${s}\r\n\r\n`;
223,404✔
165
  }
120,215✔
166

167
  encode(): Uint8Array {
54✔
168
    return TE.encode(this.toString());
120,214✔
169
  }
120,214✔
170

171
  static validHeaderValue(k: string): string {
54✔
172
    const inv = /[\r\n]/;
1,338✔
173
    if (inv.test(k)) {
491!
174
      throw InvalidArgumentError.format(
493✔
175
        "header",
493✔
176
        "values cannot contain \\r or \\n",
493✔
177
      );
178
    }
493✔
179
    return k.trim();
1,794✔
180
  }
1,338✔
181

182
  keys(): string[] {
54✔
183
    const keys = [];
2,494✔
184
    for (const sk of this.headers.keys()) {
2,494✔
185
      keys.push(sk);
4,597✔
186
    }
4,597✔
187
    return keys;
2,494✔
188
  }
2,494✔
189

190
  findKeys(k: string, match = Match.Exact): string[] {
54✔
191
    const keys = this.keys();
2,492✔
192
    switch (match) {
2,492✔
193
      case Match.Exact:
2,492✔
194
        return keys.filter((v) => {
3,417✔
195
          return v === k;
5,502✔
196
        });
3,417✔
197
      case Match.CanonicalMIME:
977!
198
        k = canonicalMIMEHeaderKey(k);
985✔
199
        return keys.filter((v) => {
985✔
200
          return v === k;
990✔
201
        });
985✔
202
      default: {
1,965!
203
        const lci = k.toLowerCase();
988✔
204
        return keys.filter((v) => {
988✔
205
          return lci === v.toLowerCase();
998✔
206
        });
988✔
207
      }
988✔
208
    }
2,492✔
209
  }
2,492✔
210

211
  get(k: string, match = Match.Exact): string {
54✔
212
    const keys = this.findKeys(k, match);
664✔
213
    if (keys.length) {
664✔
214
      const v = this.headers.get(keys[0]);
1,183✔
215
      if (v) {
1,183✔
216
        return Array.isArray(v) ? v[0] : v;
×
217
      }
1,183✔
218
    }
1,183!
219
    return "";
735✔
220
  }
653✔
221

222
  last(k: string, match = Match.Exact): string {
52✔
223
    const keys = this.findKeys(k, match);
125✔
224
    if (keys.length) {
125✔
225
      const v = this.headers.get(keys[0]);
128✔
226
      if (v) {
128✔
227
        return Array.isArray(v) ? v[v.length - 1] : v;
×
228
      }
128✔
229
    }
128!
230
    return "";
38✔
231
  }
37✔
232

233
  has(k: string, match = Match.Exact): boolean {
33✔
234
    return this.findKeys(k, match).length > 0;
46✔
235
  }
46✔
236

237
  set(k: string, v: string, match = Match.Exact): void {
54✔
238
    this.delete(k, match);
485✔
239
    this.append(k, v, match);
485✔
240
  }
485✔
241

242
  append(k: string, v: string, match = Match.Exact): void {
54✔
243
    // validate the key
244
    const ck = canonicalMIMEHeaderKey(k);
1,341✔
245
    if (match === Match.CanonicalMIME) {
494!
246
      k = ck;
498✔
247
    }
498✔
248
    // if we get non-sensical ignores/etc, we should try
249
    // to do the right thing and use the first key that matches
250
    const keys = this.findKeys(k, match);
1,799✔
251
    k = keys.length > 0 ? keys[0] : k;
881!
252

253
    const value = MsgHdrsImpl.validHeaderValue(v);
1,341✔
254
    let a = this.headers.get(k);
1,341✔
255
    if (!a) {
1,341✔
256
      a = [];
2,166✔
257
      this.headers.set(k, a);
2,166✔
258
    }
2,166✔
259
    a.push(value);
1,797✔
260
  }
1,341✔
261

262
  values(k: string, match = Match.Exact): string[] {
34✔
263
    const buf: string[] = [];
58✔
264
    const keys = this.findKeys(k, match);
58✔
265
    keys.forEach((v) => {
58✔
266
      const values = this.headers.get(v);
83✔
267
      if (values) {
83✔
268
        buf.push(...values);
83✔
269
      }
83✔
270
    });
58✔
271
    return buf;
58✔
272
  }
58✔
273

274
  delete(k: string, match = Match.Exact): void {
54✔
275
    const keys = this.findKeys(k, match);
488✔
276
    keys.forEach((v) => {
267✔
277
      this.headers.delete(v);
274✔
278
    });
267✔
279
  }
488✔
280

281
  get hasError(): boolean {
33✔
282
    return this._code >= 300;
38✔
283
  }
38✔
284

285
  get status(): string {
33✔
286
    return `${this._code} ${this._description}`.trim();
39✔
287
  }
39✔
288

289
  toRecord(): Record<string, string[]> {
34✔
290
    const data = {} as Record<string, string[]>;
36✔
291
    this.keys().forEach((v) => {
36✔
292
      data[v] = this.values(v);
39✔
293
    });
36✔
294
    return data;
36✔
295
  }
36✔
296

297
  get code(): number {
53✔
298
    return this._code;
572✔
299
  }
572✔
300

301
  get description(): string {
52✔
302
    return this._description;
240✔
303
  }
240✔
304

305
  static fromRecord(r: Record<string, string[]>): MsgHdrs {
34✔
306
    const h = new MsgHdrsImpl();
36✔
307
    for (const k in r) {
36✔
308
      h.headers.set(k, r[k]);
38✔
309
    }
38✔
310
    return h;
36✔
311
  }
36✔
312
}
55✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc