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

pmcelhaney / counterfact / 21177615739

20 Jan 2026 03:40PM UTC coverage: 81.945% (-2.3%) from 84.197%
21177615739

push

github

pmcelhaney
add an HTTP client (client) to the REPL

1157 of 1291 branches covered (89.62%)

Branch coverage included in aggregate %.

56 of 223 new or added lines in 2 files covered. (25.11%)

3613 of 4530 relevant lines covered (79.76%)

57.38 hits per line

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

24.77
/src/repl/RawHttpClient.ts
1
import net from "net";
2✔
2

2✔
3
const colors = {
2✔
4
  reset: "\x1b[0m",
2✔
5
  dim: "\x1b[2m",
2✔
6
  cyan: "\x1b[36m",
2✔
7
  green: "\x1b[32m",
2✔
8
  yellow: "\x1b[33m",
2✔
9
  red: "\x1b[31m",
2✔
10
  bold: "\x1b[1m",
2✔
11
  magenta: "\x1b[35m",
2✔
12
  blue: "\x1b[34m",
2✔
13
};
2✔
14

2✔
NEW
15
function isLikelyJson(headersBlock: string, body: string) {
×
NEW
16
  const m = headersBlock.match(/^content-type:\s*([^\r\n;]+)/im);
×
NEW
17
  const ct = (m?.[1] ?? "").toLowerCase();
×
NEW
18
  if (ct.includes("application/json") || ct.includes("+json")) return true;
×
NEW
19

×
NEW
20
  const s = body.trim();
×
NEW
21
  if (!s) return false;
×
NEW
22
  return (
×
NEW
23
    (s.startsWith("{") && s.endsWith("}")) ||
×
NEW
24
    (s.startsWith("[") && s.endsWith("]"))
×
NEW
25
  );
×
NEW
26
}
×
27

2✔
NEW
28
function highlightJson(text: string) {
×
NEW
29
  let obj;
×
NEW
30
  try {
×
NEW
31
    obj = JSON.parse(text);
×
NEW
32
  } catch {
×
NEW
33
    return text;
×
NEW
34
  }
×
NEW
35

×
NEW
36
  const pretty = JSON.stringify(obj, null, 2);
×
NEW
37

×
NEW
38
  return pretty.replace(
×
NEW
39
    /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,
×
NEW
40
    (match, str, colon, boolOrNull) => {
×
NEW
41
      if (str) {
×
NEW
42
        if (colon) return `${colors.blue}${str}${colors.reset}${colon}`;
×
NEW
43
        return `${colors.green}${str}${colors.reset}`;
×
NEW
44
      }
×
NEW
45
      if (boolOrNull) {
×
NEW
46
        return `${colors.magenta}${match}${colors.reset}`;
×
NEW
47
      }
×
NEW
48
      return `${colors.yellow}${match}${colors.reset}`;
×
NEW
49
    },
×
NEW
50
  );
×
NEW
51
}
×
52

2✔
NEW
53
function stringifyBody(body: any) {
×
NEW
54
  if (typeof body === "string") {
×
NEW
55
    return body;
×
NEW
56
  }
×
NEW
57

×
NEW
58
  if (typeof body === "undefined") {
×
NEW
59
    return body;
×
NEW
60
  }
×
NEW
61

×
NEW
62
  return JSON.stringify(body);
×
NEW
63
}
×
64

2✔
65
export class RawHttpClient {
2✔
66
  host: string;
2✔
67
  port: number;
28✔
68
  requestNumber = 0;
28✔
69

2✔
70
  constructor(host = "localhost", port = 80) {
2✔
71
    this.host = host;
28✔
72
    this.port = port;
28✔
73
  }
28✔
74

2✔
75
  get(path: string, headers = {}) {
2✔
NEW
76
    this.#send("GET", path, "", headers);
×
NEW
77
  }
×
78

2✔
79
  head(path: string, headers = {}) {
2✔
NEW
80
    this.#send("HEAD", path, "", headers);
×
NEW
81
  }
×
82

2✔
83
  post(path: string, body = "", headers = {}) {
2✔
NEW
84
    this.#send("POST", path, body, headers);
×
NEW
85
  }
×
86

2✔
87
  put(path: string, body = "", headers = {}) {
2✔
NEW
88
    this.#send("PUT", path, body, headers);
×
NEW
89
  }
×
90

2✔
91
  delete(path: string, headers = {}) {
2✔
NEW
92
    this.#send("DELETE", path, "", headers);
×
NEW
93
  }
×
94

2✔
95
  connect(path: string, headers = {}) {
2✔
NEW
96
    this.#send("CONNECT", path, "", headers);
×
NEW
97
  }
×
98

2✔
99
  options(path: string, headers = {}) {
2✔
NEW
100
    this.#send("OPTIONS", path, "", headers);
×
NEW
101
  }
×
102

2✔
103
  trace(path: string, headers = {}) {
2✔
NEW
104
    this.#send("TRACE", path, "", headers);
×
NEW
105
  }
×
106

2✔
107
  patch(path: string, body = "", headers = {}) {
2✔
NEW
108
    this.#send("PATCH", path, body, headers);
×
NEW
109
  }
×
110

2✔
111
  #send(
2✔
NEW
112
    method: string,
×
NEW
113
    path: string,
×
NEW
114
    bodyAsStringOrObject: any,
×
NEW
115
    headers: Record<string, string>,
×
NEW
116
  ) {
×
NEW
117
    const requestNumber = ++this.requestNumber;
×
NEW
118

×
NEW
119
    const body = stringifyBody(bodyAsStringOrObject);
×
NEW
120

×
NEW
121
    return new Promise((resolve, reject) => {
×
NEW
122
      const socket = net.createConnection(
×
NEW
123
        { host: this.host, port: this.port },
×
NEW
124
        () => {
×
NEW
125
          let request = `${method} ${path} HTTP/1.1\r\n`;
×
NEW
126
          request += `Host: ${this.host}\r\n`;
×
NEW
127
          request += `Connection: close\r\n`;
×
NEW
128

×
NEW
129
          if (body != null) {
×
NEW
130
            request += `Content-Length: ${Buffer.byteLength(body)}\r\n`;
×
NEW
131
          }
×
NEW
132

×
NEW
133
          for (const [key, value] of Object.entries(headers)) {
×
NEW
134
            request += `${key}: ${value}\r\n`;
×
NEW
135
          }
×
NEW
136

×
NEW
137
          request += `\r\n`;
×
NEW
138
          if (body != null) request += body;
×
NEW
139

×
NEW
140
          this.#printRequest(request, requestNumber);
×
NEW
141
          socket.write(request);
×
NEW
142
        },
×
NEW
143
      );
×
NEW
144

×
NEW
145
      const chunks: Uint8Array<ArrayBufferLike>[] = [];
×
NEW
146

×
NEW
147
      socket.on("data", (chunk: Uint8Array<ArrayBufferLike>) => {
×
NEW
148
        chunks.push(chunk);
×
NEW
149
      });
×
NEW
150

×
NEW
151
      socket.on("end", () => {
×
NEW
152
        const raw = Buffer.concat(chunks).toString("utf8");
×
NEW
153
        this.#printResponse(raw, requestNumber);
×
NEW
154
        process.stdout.write(`${colors.bold}⬣> ${colors.reset}`);
×
NEW
155
        resolve(raw);
×
NEW
156
      });
×
NEW
157

×
NEW
158
      socket.on("error", reject);
×
NEW
159
    });
×
NEW
160
  }
×
161

2✔
162
  #printRequest(raw: string, requestNumber: number) {
2✔
NEW
163
    const [head = "", body = ""] = raw.split("\r\n\r\n");
×
NEW
164
    const lines = head.split("\r\n");
×
NEW
165

×
NEW
166
    process.stdout.write(
×
NEW
167
      `${colors.cyan}\n----- REQUEST #${requestNumber} -----${colors.reset}\n`,
×
NEW
168
    );
×
NEW
169
    process.stdout.write(`${colors.cyan}${lines[0]}${colors.reset}\n`);
×
NEW
170

×
NEW
171
    for (const line of lines.slice(1)) {
×
NEW
172
      process.stdout.write(`${colors.dim}${line}${colors.reset}\n`);
×
NEW
173
    }
×
NEW
174

×
NEW
175
    if (body) {
×
NEW
176
      process.stdout.write("\n");
×
NEW
177

×
NEW
178
      const outBody = isLikelyJson(head.toLowerCase(), body)
×
NEW
179
        ? highlightJson(body)
×
NEW
180
        : body;
×
NEW
181

×
NEW
182
      process.stdout.write(outBody + "\n");
×
NEW
183
    }
×
NEW
184
  }
×
185

2✔
186
  #printResponse(raw: string, requestNumber: number) {
2✔
NEW
187
    const [head = "", body = ""] = raw.split("\r\n\r\n");
×
NEW
188
    const lines = head.split("\r\n");
×
NEW
189
    const statusLine = lines[0] ?? "";
×
NEW
190

×
NEW
191
    let statusColor = colors.green;
×
NEW
192
    const match = statusLine.match(/HTTP\/\d+\.\d+\s+(\d+)/);
×
NEW
193
    if (match) {
×
NEW
194
      const code = Number(match[1]);
×
NEW
195
      if (code >= 400) statusColor = colors.red;
×
NEW
196
      else if (code >= 300) statusColor = colors.yellow;
×
NEW
197
    }
×
NEW
198

×
NEW
199
    process.stdout.write(
×
NEW
200
      `\n${statusColor}----- RESPONSE #${requestNumber} -----${colors.reset}\n`,
×
NEW
201
    );
×
NEW
202
    process.stdout.write(`${statusColor}${statusLine}${colors.reset}\n`);
×
NEW
203

×
NEW
204
    for (const line of lines.slice(1)) {
×
NEW
205
      process.stdout.write(`${colors.dim}${line}${colors.reset}\n`);
×
NEW
206
    }
×
NEW
207

×
NEW
208
    if (body) {
×
NEW
209
      process.stdout.write("\n");
×
NEW
210

×
NEW
211
      const outBody = isLikelyJson(head!.toLowerCase(), body)
×
NEW
212
        ? highlightJson(body)
×
NEW
213
        : body;
×
NEW
214

×
NEW
215
      process.stdout.write(outBody + "\n");
×
NEW
216
    }
×
NEW
217
  }
×
218
}
2✔
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