@continuedev/fetch 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/stream.ts CHANGED
@@ -24,6 +24,7 @@ export async function* streamResponse(
24
24
 
25
25
  // Get the major version of Node.js
26
26
  const nodeMajorVersion = parseInt(process.versions.node.split(".")[0], 10);
27
+ let chunks = 0;
27
28
 
28
29
  try {
29
30
  if (nodeMajorVersion >= 20) {
@@ -33,6 +34,7 @@ export async function* streamResponse(
33
34
  new TextDecoderStream("utf-8"),
34
35
  )) {
35
36
  yield chunk;
37
+ chunks++;
36
38
  }
37
39
  } else {
38
40
  // Fallback for Node versions below 20
@@ -41,11 +43,30 @@ export async function* streamResponse(
41
43
  const nodeStream = response.body as unknown as NodeJS.ReadableStream;
42
44
  for await (const chunk of toAsyncIterable(nodeStream)) {
43
45
  yield decoder.decode(chunk, { stream: true });
46
+ chunks++;
44
47
  }
45
48
  }
46
49
  } catch (e) {
47
- if (e instanceof Error && e.name.startsWith("AbortError")) {
48
- return; // In case of client-side cancellation, just return
50
+ if (e instanceof Error) {
51
+ if (e.name.startsWith("AbortError")) {
52
+ return; // In case of client-side cancellation, just return
53
+ }
54
+ if (e.message.toLowerCase().includes("premature close")) {
55
+ // Premature close can happen for various reasons, including:
56
+ // - Malformed chunks of data received from the server
57
+ // - The server closed the connection before sending the complete response
58
+ // - Long delays from the server during streaming
59
+ // - 'Keep alive' header being used in combination with an http agent and a set, low number of maxSockets
60
+ if (chunks === 0) {
61
+ throw new Error(
62
+ "Stream was closed before any data was received. Try again. (Premature Close)",
63
+ );
64
+ } else {
65
+ throw new Error(
66
+ "The response was cancelled mid-stream. Try again. (Premature Close).",
67
+ );
68
+ }
69
+ }
49
70
  }
50
71
  throw e;
51
72
  }
@@ -126,8 +147,12 @@ export async function* streamJSON(response: Response): AsyncGenerator<any> {
126
147
  let position;
127
148
  while ((position = buffer.indexOf("\n")) >= 0) {
128
149
  const line = buffer.slice(0, position);
129
- const data = JSON.parse(line);
130
- yield data;
150
+ try {
151
+ const data = JSON.parse(line);
152
+ yield data;
153
+ } catch (e) {
154
+ throw new Error(`Malformed JSON sent from server: ${line}`);
155
+ }
131
156
  buffer = buffer.slice(position + 1);
132
157
  }
133
158
  }
package/src/util.test.ts CHANGED
@@ -1,9 +1,13 @@
1
- import { jest } from "@jest/globals";
2
- import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
1
+ import { afterEach, expect, test, vi } from "vitest";
2
+ import {
3
+ getProxyFromEnv,
4
+ patternMatchesHostname,
5
+ shouldBypassProxy,
6
+ } from "./util.js";
3
7
 
4
8
  // Reset environment variables after each test
5
9
  afterEach(() => {
6
- jest.resetModules();
10
+ vi.resetModules();
7
11
  process.env = {};
8
12
  });
9
13
 
@@ -52,51 +56,159 @@ test("getProxyFromEnv prefers HTTPS_PROXY over other env vars for https protocol
52
56
  expect(getProxyFromEnv("https:")).toBe("https://preferred.example.com");
53
57
  });
54
58
 
59
+ // Tests for patternMatchesHostname
60
+ test("patternMatchesHostname with exact hostname match", () => {
61
+ expect(patternMatchesHostname("example.com", "example.com")).toBe(true);
62
+ expect(patternMatchesHostname("example.com", "different.com")).toBe(false);
63
+ });
64
+
65
+ test("patternMatchesHostname with wildcard domains", () => {
66
+ expect(patternMatchesHostname("sub.example.com", "*.example.com")).toBe(true);
67
+ expect(patternMatchesHostname("sub.sub.example.com", "*.example.com")).toBe(
68
+ true,
69
+ );
70
+ expect(patternMatchesHostname("example.com", "*.example.com")).toBe(false);
71
+ expect(patternMatchesHostname("sub.different.com", "*.example.com")).toBe(
72
+ false,
73
+ );
74
+ });
75
+
76
+ test("patternMatchesHostname with domain suffix", () => {
77
+ expect(patternMatchesHostname("sub.example.com", ".example.com")).toBe(true);
78
+ expect(patternMatchesHostname("example.com", ".example.com")).toBe(true);
79
+ expect(patternMatchesHostname("different.com", ".example.com")).toBe(false);
80
+ });
81
+
82
+ test("patternMatchesHostname with case insensitivity", () => {
83
+ expect(patternMatchesHostname("EXAMPLE.com", "example.COM")).toBe(true);
84
+ expect(patternMatchesHostname("sub.EXAMPLE.com", "*.example.COM")).toBe(true);
85
+ });
86
+
87
+ // Port handling tests
88
+ test("patternMatchesHostname with exact port match", () => {
89
+ expect(patternMatchesHostname("example.com:8080", "example.com:8080")).toBe(
90
+ true,
91
+ );
92
+ expect(patternMatchesHostname("example.com:8080", "example.com:9090")).toBe(
93
+ false,
94
+ );
95
+ });
96
+
97
+ test("patternMatchesHostname with port in pattern but not in hostname", () => {
98
+ expect(patternMatchesHostname("example.com", "example.com:8080")).toBe(false);
99
+ });
100
+
101
+ test("patternMatchesHostname with port in hostname but not in pattern", () => {
102
+ expect(patternMatchesHostname("example.com:8080", "example.com")).toBe(true);
103
+ });
104
+
105
+ test("patternMatchesHostname with wildcard domains and ports", () => {
106
+ expect(
107
+ patternMatchesHostname("sub.example.com:8080", "*.example.com:8080"),
108
+ ).toBe(true);
109
+ expect(
110
+ patternMatchesHostname("sub.example.com:9090", "*.example.com:8080"),
111
+ ).toBe(false);
112
+ expect(patternMatchesHostname("sub.example.com", "*.example.com:8080")).toBe(
113
+ false,
114
+ );
115
+ });
116
+
117
+ test("patternMatchesHostname with domain suffix and ports", () => {
118
+ expect(
119
+ patternMatchesHostname("sub.example.com:8080", ".example.com:8080"),
120
+ ).toBe(true);
121
+ expect(patternMatchesHostname("example.com:8080", ".example.com:8080")).toBe(
122
+ true,
123
+ );
124
+ expect(
125
+ patternMatchesHostname("sub.example.com:9090", ".example.com:8080"),
126
+ ).toBe(false);
127
+ });
128
+
55
129
  // Tests for shouldBypassProxy
56
130
  test("shouldBypassProxy returns false when NO_PROXY is not set", () => {
57
- expect(shouldBypassProxy("example.com")).toBe(false);
131
+ expect(shouldBypassProxy("example.com", undefined)).toBe(false);
58
132
  });
59
133
 
60
134
  test("shouldBypassProxy returns true for exact hostname match", () => {
61
135
  process.env.NO_PROXY = "example.com,another.com";
62
- expect(shouldBypassProxy("example.com")).toBe(true);
136
+ expect(shouldBypassProxy("example.com", undefined)).toBe(true);
63
137
  });
64
138
 
65
139
  test("shouldBypassProxy returns false when hostname doesn't match any NO_PROXY entry", () => {
66
140
  process.env.NO_PROXY = "example.com,another.com";
67
- expect(shouldBypassProxy("different.com")).toBe(false);
141
+ expect(shouldBypassProxy("different.com", undefined)).toBe(false);
68
142
  });
69
143
 
70
144
  test("shouldBypassProxy handles lowercase no_proxy", () => {
71
145
  process.env.no_proxy = "example.com";
72
- expect(shouldBypassProxy("example.com")).toBe(true);
146
+ expect(shouldBypassProxy("example.com", undefined)).toBe(true);
73
147
  });
74
148
 
75
149
  test("shouldBypassProxy works with wildcard domains", () => {
76
150
  process.env.NO_PROXY = "*.example.com";
77
- expect(shouldBypassProxy("sub.example.com")).toBe(true);
78
- expect(shouldBypassProxy("example.com")).toBe(false);
79
- expect(shouldBypassProxy("different.com")).toBe(false);
151
+ expect(shouldBypassProxy("sub.example.com", undefined)).toBe(true);
152
+ expect(shouldBypassProxy("example.com", undefined)).toBe(false);
153
+ expect(shouldBypassProxy("different.com", undefined)).toBe(false);
80
154
  });
81
155
 
82
156
  test("shouldBypassProxy works with domain suffix", () => {
83
157
  process.env.NO_PROXY = ".example.com";
84
- expect(shouldBypassProxy("sub.example.com")).toBe(true);
85
- expect(shouldBypassProxy("example.com")).toBe(true);
86
- expect(shouldBypassProxy("different.com")).toBe(false);
158
+ expect(shouldBypassProxy("sub.example.com", undefined)).toBe(true);
159
+ expect(shouldBypassProxy("example.com", undefined)).toBe(true);
160
+ expect(shouldBypassProxy("different.com", undefined)).toBe(false);
87
161
  });
88
162
 
89
163
  test("shouldBypassProxy handles multiple entries with different patterns", () => {
90
164
  process.env.NO_PROXY = "internal.local,*.example.com,.test.com";
91
- expect(shouldBypassProxy("internal.local")).toBe(true);
92
- expect(shouldBypassProxy("sub.example.com")).toBe(true);
93
- expect(shouldBypassProxy("sub.test.com")).toBe(true);
94
- expect(shouldBypassProxy("test.com")).toBe(true);
95
- expect(shouldBypassProxy("example.org")).toBe(false);
165
+ expect(shouldBypassProxy("internal.local", undefined)).toBe(true);
166
+ expect(shouldBypassProxy("sub.example.com", undefined)).toBe(true);
167
+ expect(shouldBypassProxy("sub.test.com", undefined)).toBe(true);
168
+ expect(shouldBypassProxy("test.com", undefined)).toBe(true);
169
+ expect(shouldBypassProxy("example.org", undefined)).toBe(false);
96
170
  });
97
171
 
98
172
  test("shouldBypassProxy ignores whitespace in NO_PROXY", () => {
99
173
  process.env.NO_PROXY = " example.com , *.test.org ";
100
- expect(shouldBypassProxy("example.com")).toBe(true);
101
- expect(shouldBypassProxy("subdomain.test.org")).toBe(true);
174
+ expect(shouldBypassProxy("example.com", undefined)).toBe(true);
175
+ expect(shouldBypassProxy("subdomain.test.org", undefined)).toBe(true);
176
+ });
177
+
178
+ test("shouldBypassProxy with ports in NO_PROXY", () => {
179
+ process.env.NO_PROXY = "example.com:8080,*.test.org:443,.internal.net:8443";
180
+ expect(shouldBypassProxy("example.com:8080", undefined)).toBe(true);
181
+ expect(shouldBypassProxy("example.com:9090", undefined)).toBe(false);
182
+ expect(shouldBypassProxy("sub.test.org:443", undefined)).toBe(true);
183
+ expect(shouldBypassProxy("sub.internal.net:8443", undefined)).toBe(true);
184
+ expect(shouldBypassProxy("internal.net:8443", undefined)).toBe(true);
185
+ });
186
+
187
+ test("shouldBypassProxy accepts options with noProxy patterns", () => {
188
+ const options = { noProxy: ["example.com:8080", "*.internal.net"] };
189
+ expect(shouldBypassProxy("example.com:8080", options)).toBe(true);
190
+ expect(shouldBypassProxy("example.com", options)).toBe(false);
191
+ expect(shouldBypassProxy("server.internal.net", options)).toBe(true);
192
+ });
193
+
194
+ test("shouldBypassProxy combines environment and options noProxy patterns", () => {
195
+ process.env.NO_PROXY = "example.org,*.test.com";
196
+ const options = { noProxy: ["example.com:8080", "*.internal.net"] };
197
+ expect(shouldBypassProxy("example.org", options)).toBe(true);
198
+ expect(shouldBypassProxy("sub.test.com", options)).toBe(true);
199
+ expect(shouldBypassProxy("example.com:8080", options)).toBe(true);
200
+ expect(shouldBypassProxy("server.internal.net", options)).toBe(true);
201
+ expect(shouldBypassProxy("other.domain", options)).toBe(false);
202
+ });
203
+
204
+ test("shouldBypassProxy handles empty noProxy array in options", () => {
205
+ process.env.NO_PROXY = "example.org";
206
+ const options = { noProxy: [] };
207
+ expect(shouldBypassProxy("example.org", options)).toBe(true);
208
+ expect(shouldBypassProxy("different.com", options)).toBe(false);
209
+ });
210
+
211
+ test("shouldBypassProxy handles undefined options", () => {
212
+ process.env.NO_PROXY = "example.org";
213
+ expect(shouldBypassProxy("example.org", undefined)).toBe(true);
102
214
  });
package/src/util.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { RequestOptions } from "@continuedev/config-types";
2
+
1
3
  /**
2
4
  * Gets the proxy settings from environment variables
3
5
  * @param protocol The URL protocol (http: or https:)
@@ -16,28 +18,91 @@ export function getProxyFromEnv(protocol: string): string | undefined {
16
18
  }
17
19
  }
18
20
 
19
- /**
20
- * Checks if a hostname should bypass proxy based on NO_PROXY environment variable
21
- * @param hostname The hostname to check
22
- * @returns True if the hostname should bypass proxy
23
- */
24
- export function shouldBypassProxy(hostname: string): boolean {
25
- const noProxy = process.env.NO_PROXY || process.env.no_proxy;
26
- if (!noProxy) return false;
27
-
28
- const noProxyItems = noProxy.split(",").map((item) => item.trim());
21
+ // Note that request options proxy (per model) takes precedence over environment variables
22
+ export function getProxy(
23
+ protocol: string,
24
+ requestOptions?: RequestOptions,
25
+ ): string | undefined {
26
+ if (requestOptions?.proxy) {
27
+ return requestOptions.proxy;
28
+ }
29
+ return getProxyFromEnv(protocol);
30
+ }
29
31
 
30
- return noProxyItems.some((item) => {
31
- // Exact match
32
- if (item === hostname) return true;
32
+ export function getEnvNoProxyPatterns(): string[] {
33
+ const envValue = process.env.NO_PROXY || process.env.no_proxy;
34
+ if (envValue) {
35
+ return envValue
36
+ .split(",")
37
+ .map((item) => item.trim().toLowerCase())
38
+ .filter((i) => !!i);
39
+ } else {
40
+ return [];
41
+ }
42
+ }
33
43
 
34
- // Wildcard domain match (*.example.com)
35
- if (item.startsWith("*.") && hostname.endsWith(item.substring(1)))
36
- return true;
44
+ export function getReqOptionsNoProxyPatterns(
45
+ options: RequestOptions | undefined,
46
+ ): string[] {
47
+ return (
48
+ options?.noProxy?.map((i) => i.trim().toLowerCase()).filter((i) => !!i) ??
49
+ []
50
+ );
51
+ }
37
52
 
38
- // Domain suffix match (.example.com)
39
- if (item.startsWith(".") && hostname.endsWith(item.slice(1))) return true;
53
+ export function patternMatchesHostname(hostname: string, pattern: string) {
54
+ // Split hostname and pattern to separate hostname and port
55
+ const [hostnameWithoutPort, hostnamePort] = hostname.toLowerCase().split(":");
56
+ const [patternWithoutPort, patternPort] = pattern.toLowerCase().split(":");
40
57
 
58
+ // If pattern specifies a port but hostname doesn't match it, no match
59
+ if (patternPort && (!hostnamePort || hostnamePort !== patternPort)) {
41
60
  return false;
42
- });
61
+ }
62
+
63
+ // Now compare just the hostname parts
64
+
65
+ // exact match
66
+ if (patternWithoutPort === hostnameWithoutPort) {
67
+ return true;
68
+ }
69
+ // wildcard domain match (*.example.com)
70
+ if (
71
+ patternWithoutPort.startsWith("*.") &&
72
+ hostnameWithoutPort.endsWith(patternWithoutPort.substring(1))
73
+ ) {
74
+ return true;
75
+ }
76
+ // Domain suffix match (.example.com)
77
+ if (
78
+ patternWithoutPort.startsWith(".") &&
79
+ hostnameWithoutPort.endsWith(patternWithoutPort.slice(1))
80
+ ) {
81
+ return true;
82
+ }
83
+
84
+ // TODO IP address ranges
85
+
86
+ // TODO CIDR notation
87
+
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * Checks if a hostname should bypass proxy based on NO_PROXY environment variable
93
+ * @param hostname The hostname to check
94
+ * @returns True if the hostname should bypass proxy
95
+ */
96
+ export function shouldBypassProxy(
97
+ hostname: string,
98
+ requestOptions: RequestOptions | undefined,
99
+ ): boolean {
100
+ const ignores = [
101
+ ...getEnvNoProxyPatterns(),
102
+ ...getReqOptionsNoProxyPatterns(requestOptions),
103
+ ];
104
+ const hostLowerCase = hostname.toLowerCase();
105
+ return ignores.some((ignore) =>
106
+ patternMatchesHostname(hostLowerCase, ignore),
107
+ );
43
108
  }
package/tsconfig.json CHANGED
@@ -14,10 +14,10 @@
14
14
  "resolveJsonModule": true,
15
15
  "isolatedModules": true,
16
16
  "noEmitOnError": false,
17
- "types": ["node", "jest"],
17
+ "types": ["node"],
18
18
  "outDir": "dist",
19
19
  "declaration": true
20
20
  // "sourceMap": true
21
21
  },
22
- "include": ["src/**/*", "jest.globals.d.ts"]
22
+ "include": ["src/**/*"]
23
23
  }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ timeout: 10000,
7
+ globals: true,
8
+ },
9
+ resolve: {
10
+ alias: {
11
+ // Handle .js imports for TypeScript files
12
+ "./getAgentOptions.js": "./getAgentOptions.ts",
13
+ "./stream.js": "./stream.ts",
14
+ "./util.js": "./util.ts",
15
+ },
16
+ },
17
+ });
package/jest.config.mjs DELETED
@@ -1,20 +0,0 @@
1
- import path from "path";
2
- import { fileURLToPath } from "url";
3
-
4
- export default {
5
- transform: {
6
- "\\.[jt]sx?$": ["ts-jest", { useESM: true }],
7
- },
8
-
9
- moduleNameMapper: {
10
- "(.+)\\.js": "$1",
11
- },
12
- extensionsToTreatAsEsm: [".ts"],
13
- preset: "ts-jest/presets/default-esm",
14
- testTimeout: 10000,
15
- testEnvironment: "node",
16
- globals: {
17
- __dirname: path.dirname(fileURLToPath(import.meta.url)),
18
- __filename: path.resolve(fileURLToPath(import.meta.url)),
19
- },
20
- };
package/jest.globals.d.ts DELETED
@@ -1,14 +0,0 @@
1
- // This file declares the Jest globals for TypeScript
2
- import '@jest/globals';
3
-
4
- declare global {
5
- const describe: typeof import('@jest/globals').describe;
6
- const expect: typeof import('@jest/globals').expect;
7
- const it: typeof import('@jest/globals').it;
8
- const test: typeof import('@jest/globals').test;
9
- const beforeAll: typeof import('@jest/globals').beforeAll;
10
- const afterAll: typeof import('@jest/globals').afterAll;
11
- const beforeEach: typeof import('@jest/globals').beforeEach;
12
- const afterEach: typeof import('@jest/globals').afterEach;
13
- const jest: typeof import('@jest/globals').jest;
14
- }