@continuedev/fetch 1.0.9 → 1.0.11
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/dist/fetch.js +70 -1
- package/dist/stream.js +2 -2
- package/dist/stream.test.js +53 -3
- package/jest.globals.d.ts +14 -0
- package/package.json +1 -1
- package/src/fetch.ts +89 -2
- package/src/stream.test.ts +63 -3
- package/src/stream.ts +3 -2
- package/tsconfig.json +1 -1
package/dist/fetch.js
CHANGED
|
@@ -5,6 +5,61 @@ import fetch, { Response } from "node-fetch";
|
|
|
5
5
|
import { getAgentOptions } from "./getAgentOptions.js";
|
|
6
6
|
import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
|
|
7
7
|
const { http, https } = followRedirects.default;
|
|
8
|
+
function logRequest(method, url, headers, body, proxy, shouldBypass) {
|
|
9
|
+
console.log("=== FETCH REQUEST ===");
|
|
10
|
+
console.log(`Method: ${method}`);
|
|
11
|
+
console.log(`URL: ${url.toString()}`);
|
|
12
|
+
// Log headers in curl format
|
|
13
|
+
console.log("Headers:");
|
|
14
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
15
|
+
console.log(` -H '${key}: ${value}'`);
|
|
16
|
+
}
|
|
17
|
+
// Log proxy information
|
|
18
|
+
if (proxy && !shouldBypass) {
|
|
19
|
+
console.log(`Proxy: ${proxy}`);
|
|
20
|
+
}
|
|
21
|
+
// Log body
|
|
22
|
+
if (body) {
|
|
23
|
+
console.log(`Body: ${body}`);
|
|
24
|
+
}
|
|
25
|
+
// Generate equivalent curl command
|
|
26
|
+
let curlCommand = `curl -X ${method}`;
|
|
27
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
28
|
+
curlCommand += ` -H '${key}: ${value}'`;
|
|
29
|
+
}
|
|
30
|
+
if (body) {
|
|
31
|
+
curlCommand += ` -d '${body}'`;
|
|
32
|
+
}
|
|
33
|
+
if (proxy && !shouldBypass) {
|
|
34
|
+
curlCommand += ` --proxy '${proxy}'`;
|
|
35
|
+
}
|
|
36
|
+
curlCommand += ` '${url.toString()}'`;
|
|
37
|
+
console.log(`Equivalent curl: ${curlCommand}`);
|
|
38
|
+
console.log("=====================");
|
|
39
|
+
}
|
|
40
|
+
async function logResponse(resp) {
|
|
41
|
+
console.log("=== FETCH RESPONSE ===");
|
|
42
|
+
console.log(`Status: ${resp.status} ${resp.statusText}`);
|
|
43
|
+
console.log("Response Headers:");
|
|
44
|
+
resp.headers.forEach((value, key) => {
|
|
45
|
+
console.log(` ${key}: ${value}`);
|
|
46
|
+
});
|
|
47
|
+
// Clone response to read body without consuming it
|
|
48
|
+
const respClone = resp.clone();
|
|
49
|
+
try {
|
|
50
|
+
const responseText = await respClone.text();
|
|
51
|
+
console.log(`Response Body: ${responseText}`);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
console.log("Could not read response body:", e);
|
|
55
|
+
}
|
|
56
|
+
console.log("======================");
|
|
57
|
+
}
|
|
58
|
+
function logError(error) {
|
|
59
|
+
console.log("=== FETCH ERROR ===");
|
|
60
|
+
console.log(`Error: ${error}`);
|
|
61
|
+
console.log("===================");
|
|
62
|
+
}
|
|
8
63
|
export async function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
9
64
|
const url = typeof url_ === "string" ? new URL(url_) : url_;
|
|
10
65
|
if (url.host === "localhost") {
|
|
@@ -52,14 +107,24 @@ export async function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
|
52
107
|
catch (e) {
|
|
53
108
|
console.log("Unable to parse HTTP request body: ", e);
|
|
54
109
|
}
|
|
110
|
+
const finalBody = updatedBody ?? init?.body;
|
|
111
|
+
const method = init?.method || "GET";
|
|
112
|
+
// Verbose logging for debugging - log request details
|
|
113
|
+
if (process.env.VERBOSE_FETCH) {
|
|
114
|
+
logRequest(method, url, headers, finalBody, proxy, shouldBypass);
|
|
115
|
+
}
|
|
55
116
|
// fetch the request with the provided options
|
|
56
117
|
try {
|
|
57
118
|
const resp = await fetch(url, {
|
|
58
119
|
...init,
|
|
59
|
-
body:
|
|
120
|
+
body: finalBody,
|
|
60
121
|
headers: headers,
|
|
61
122
|
agent: agent,
|
|
62
123
|
});
|
|
124
|
+
// Verbose logging for debugging - log response details
|
|
125
|
+
if (process.env.VERBOSE_FETCH) {
|
|
126
|
+
await logResponse(resp);
|
|
127
|
+
}
|
|
63
128
|
if (!resp.ok) {
|
|
64
129
|
const requestId = resp.headers.get("x-request-id");
|
|
65
130
|
if (requestId) {
|
|
@@ -69,6 +134,10 @@ export async function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
|
69
134
|
return resp;
|
|
70
135
|
}
|
|
71
136
|
catch (error) {
|
|
137
|
+
// Verbose logging for errors
|
|
138
|
+
if (process.env.VERBOSE_FETCH) {
|
|
139
|
+
logError(error);
|
|
140
|
+
}
|
|
72
141
|
if (error instanceof Error && error.name === "AbortError") {
|
|
73
142
|
// Return a Response object that streamResponse etc can handle
|
|
74
143
|
return new Response(null, {
|
package/dist/stream.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export async function* toAsyncIterable(nodeReadable) {
|
|
2
2
|
for await (const chunk of nodeReadable) {
|
|
3
|
-
// @ts-
|
|
3
|
+
// @ts-ignore
|
|
4
4
|
yield chunk;
|
|
5
5
|
}
|
|
6
6
|
}
|
|
@@ -64,7 +64,7 @@ export function parseDataLine(line) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
function parseSseLine(line) {
|
|
67
|
-
if (line.startsWith("data: [DONE]")) {
|
|
67
|
+
if (line.startsWith("data:[DONE]") || line.startsWith("data: [DONE]")) {
|
|
68
68
|
return { done: true, data: undefined };
|
|
69
69
|
}
|
|
70
70
|
if (line.startsWith("data:")) {
|
package/dist/stream.test.js
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Readable } from "stream";
|
|
2
|
+
import { parseDataLine, streamSse } from "./stream.js";
|
|
3
|
+
function createMockResponse(sseLines) {
|
|
4
|
+
// Create a Readable stream that emits the SSE lines
|
|
5
|
+
const stream = new Readable({
|
|
6
|
+
read() {
|
|
7
|
+
for (const line of sseLines) {
|
|
8
|
+
this.push(line + "\n\n");
|
|
9
|
+
}
|
|
10
|
+
this.push(null); // End of stream
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
// Minimal Response mock
|
|
14
|
+
return {
|
|
15
|
+
status: 200,
|
|
16
|
+
body: stream,
|
|
17
|
+
text: async () => "",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
describe("streamSse", () => {
|
|
21
|
+
it("yields parsed SSE data objects that ends with `data:[DONE]`", async () => {
|
|
22
|
+
const sseLines = [
|
|
23
|
+
'data: {"foo": "bar"}',
|
|
24
|
+
'data: {"baz": 42}',
|
|
25
|
+
"data:[DONE]",
|
|
26
|
+
];
|
|
27
|
+
const response = createMockResponse(sseLines);
|
|
28
|
+
const results = [];
|
|
29
|
+
for await (const data of streamSse(response)) {
|
|
30
|
+
results.push(data);
|
|
31
|
+
}
|
|
32
|
+
expect(results).toEqual([{ foo: "bar" }, { baz: 42 }]);
|
|
33
|
+
});
|
|
34
|
+
it("yields parsed SSE data objects that ends with `data: [DONE]` (with a space before [DONE]", async () => {
|
|
35
|
+
const sseLines = [
|
|
36
|
+
'data: {"foo": "bar"}',
|
|
37
|
+
'data: {"baz": 42}',
|
|
38
|
+
"data: [DONE]",
|
|
39
|
+
];
|
|
40
|
+
const response = createMockResponse(sseLines);
|
|
41
|
+
const results = [];
|
|
42
|
+
for await (const data of streamSse(response)) {
|
|
43
|
+
results.push(data);
|
|
44
|
+
}
|
|
45
|
+
expect(results).toEqual([{ foo: "bar" }, { baz: 42 }]);
|
|
46
|
+
});
|
|
47
|
+
it("throws on malformed JSON", async () => {
|
|
48
|
+
const sseLines = ['data: {"foo": "bar"', "data:[DONE]"];
|
|
49
|
+
const response = createMockResponse(sseLines);
|
|
50
|
+
const iterator = streamSse(response)[Symbol.asyncIterator]();
|
|
51
|
+
await expect(iterator.next()).rejects.toThrow(/Malformed JSON/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
4
54
|
describe("parseDataLine", () => {
|
|
5
55
|
test("parseDataLine should parse valid JSON data with 'data: ' prefix", () => {
|
|
6
56
|
const line = 'data: {"message":"hello","status":"ok"}';
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
package/package.json
CHANGED
package/src/fetch.ts
CHANGED
|
@@ -2,12 +2,81 @@ import { RequestOptions } from "@continuedev/config-types";
|
|
|
2
2
|
import * as followRedirects from "follow-redirects";
|
|
3
3
|
import { HttpProxyAgent } from "http-proxy-agent";
|
|
4
4
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
5
|
-
import fetch, { RequestInit, Response } from "node-fetch";
|
|
5
|
+
import fetch, { BodyInit, RequestInit, Response } from "node-fetch";
|
|
6
6
|
import { getAgentOptions } from "./getAgentOptions.js";
|
|
7
7
|
import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
|
|
8
8
|
|
|
9
9
|
const { http, https } = (followRedirects as any).default;
|
|
10
10
|
|
|
11
|
+
function logRequest(
|
|
12
|
+
method: string,
|
|
13
|
+
url: URL,
|
|
14
|
+
headers: { [key: string]: string },
|
|
15
|
+
body: BodyInit | null | undefined,
|
|
16
|
+
proxy?: string,
|
|
17
|
+
shouldBypass?: boolean,
|
|
18
|
+
) {
|
|
19
|
+
console.log("=== FETCH REQUEST ===");
|
|
20
|
+
console.log(`Method: ${method}`);
|
|
21
|
+
console.log(`URL: ${url.toString()}`);
|
|
22
|
+
|
|
23
|
+
// Log headers in curl format
|
|
24
|
+
console.log("Headers:");
|
|
25
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
26
|
+
console.log(` -H '${key}: ${value}'`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Log proxy information
|
|
30
|
+
if (proxy && !shouldBypass) {
|
|
31
|
+
console.log(`Proxy: ${proxy}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Log body
|
|
35
|
+
if (body) {
|
|
36
|
+
console.log(`Body: ${body}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Generate equivalent curl command
|
|
40
|
+
let curlCommand = `curl -X ${method}`;
|
|
41
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
42
|
+
curlCommand += ` -H '${key}: ${value}'`;
|
|
43
|
+
}
|
|
44
|
+
if (body) {
|
|
45
|
+
curlCommand += ` -d '${body}'`;
|
|
46
|
+
}
|
|
47
|
+
if (proxy && !shouldBypass) {
|
|
48
|
+
curlCommand += ` --proxy '${proxy}'`;
|
|
49
|
+
}
|
|
50
|
+
curlCommand += ` '${url.toString()}'`;
|
|
51
|
+
console.log(`Equivalent curl: ${curlCommand}`);
|
|
52
|
+
console.log("=====================");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function logResponse(resp: Response) {
|
|
56
|
+
console.log("=== FETCH RESPONSE ===");
|
|
57
|
+
console.log(`Status: ${resp.status} ${resp.statusText}`);
|
|
58
|
+
console.log("Response Headers:");
|
|
59
|
+
resp.headers.forEach((value, key) => {
|
|
60
|
+
console.log(` ${key}: ${value}`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Clone response to read body without consuming it
|
|
64
|
+
const respClone = resp.clone();
|
|
65
|
+
try {
|
|
66
|
+
const responseText = await respClone.text();
|
|
67
|
+
console.log(`Response Body: ${responseText}`);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.log("Could not read response body:", e);
|
|
70
|
+
}
|
|
71
|
+
console.log("======================");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function logError(error: unknown) {
|
|
75
|
+
console.log("=== FETCH ERROR ===");
|
|
76
|
+
console.log(`Error: ${error}`);
|
|
77
|
+
console.log("===================");
|
|
78
|
+
}
|
|
79
|
+
|
|
11
80
|
export async function fetchwithRequestOptions(
|
|
12
81
|
url_: URL | string,
|
|
13
82
|
init?: RequestInit,
|
|
@@ -68,15 +137,28 @@ export async function fetchwithRequestOptions(
|
|
|
68
137
|
console.log("Unable to parse HTTP request body: ", e);
|
|
69
138
|
}
|
|
70
139
|
|
|
140
|
+
const finalBody = updatedBody ?? init?.body;
|
|
141
|
+
const method = init?.method || "GET";
|
|
142
|
+
|
|
143
|
+
// Verbose logging for debugging - log request details
|
|
144
|
+
if (process.env.VERBOSE_FETCH) {
|
|
145
|
+
logRequest(method, url, headers, finalBody, proxy, shouldBypass);
|
|
146
|
+
}
|
|
147
|
+
|
|
71
148
|
// fetch the request with the provided options
|
|
72
149
|
try {
|
|
73
150
|
const resp = await fetch(url, {
|
|
74
151
|
...init,
|
|
75
|
-
body:
|
|
152
|
+
body: finalBody,
|
|
76
153
|
headers: headers,
|
|
77
154
|
agent: agent,
|
|
78
155
|
});
|
|
79
156
|
|
|
157
|
+
// Verbose logging for debugging - log response details
|
|
158
|
+
if (process.env.VERBOSE_FETCH) {
|
|
159
|
+
await logResponse(resp);
|
|
160
|
+
}
|
|
161
|
+
|
|
80
162
|
if (!resp.ok) {
|
|
81
163
|
const requestId = resp.headers.get("x-request-id");
|
|
82
164
|
if (requestId) {
|
|
@@ -86,6 +168,11 @@ export async function fetchwithRequestOptions(
|
|
|
86
168
|
|
|
87
169
|
return resp;
|
|
88
170
|
} catch (error) {
|
|
171
|
+
// Verbose logging for errors
|
|
172
|
+
if (process.env.VERBOSE_FETCH) {
|
|
173
|
+
logError(error);
|
|
174
|
+
}
|
|
175
|
+
|
|
89
176
|
if (error instanceof Error && error.name === "AbortError") {
|
|
90
177
|
// Return a Response object that streamResponse etc can handle
|
|
91
178
|
return new Response(null, {
|
package/src/stream.test.ts
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Readable } from "stream";
|
|
2
|
+
import { parseDataLine, streamSse } from "./stream.js";
|
|
3
|
+
|
|
4
|
+
function createMockResponse(sseLines: string[]): Response {
|
|
5
|
+
// Create a Readable stream that emits the SSE lines
|
|
6
|
+
const stream = new Readable({
|
|
7
|
+
read() {
|
|
8
|
+
for (const line of sseLines) {
|
|
9
|
+
this.push(line + "\n\n");
|
|
10
|
+
}
|
|
11
|
+
this.push(null); // End of stream
|
|
12
|
+
},
|
|
13
|
+
}) as any;
|
|
14
|
+
|
|
15
|
+
// Minimal Response mock
|
|
16
|
+
return {
|
|
17
|
+
status: 200,
|
|
18
|
+
body: stream,
|
|
19
|
+
text: async () => "",
|
|
20
|
+
} as unknown as Response;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("streamSse", () => {
|
|
24
|
+
it("yields parsed SSE data objects that ends with `data:[DONE]`", async () => {
|
|
25
|
+
const sseLines = [
|
|
26
|
+
'data: {"foo": "bar"}',
|
|
27
|
+
'data: {"baz": 42}',
|
|
28
|
+
"data:[DONE]",
|
|
29
|
+
];
|
|
30
|
+
const response = createMockResponse(sseLines);
|
|
31
|
+
|
|
32
|
+
const results = [];
|
|
33
|
+
for await (const data of streamSse(response)) {
|
|
34
|
+
results.push(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(results).toEqual([{ foo: "bar" }, { baz: 42 }]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("yields parsed SSE data objects that ends with `data: [DONE]` (with a space before [DONE]", async () => {
|
|
41
|
+
const sseLines = [
|
|
42
|
+
'data: {"foo": "bar"}',
|
|
43
|
+
'data: {"baz": 42}',
|
|
44
|
+
"data: [DONE]",
|
|
45
|
+
];
|
|
46
|
+
const response = createMockResponse(sseLines);
|
|
47
|
+
|
|
48
|
+
const results = [];
|
|
49
|
+
for await (const data of streamSse(response)) {
|
|
50
|
+
results.push(data);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
expect(results).toEqual([{ foo: "bar" }, { baz: 42 }]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("throws on malformed JSON", async () => {
|
|
57
|
+
const sseLines = ['data: {"foo": "bar"', "data:[DONE]"];
|
|
58
|
+
const response = createMockResponse(sseLines);
|
|
59
|
+
|
|
60
|
+
const iterator = streamSse(response)[Symbol.asyncIterator]();
|
|
61
|
+
await expect(iterator.next()).rejects.toThrow(/Malformed JSON/);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
4
64
|
|
|
5
65
|
describe("parseDataLine", () => {
|
|
6
66
|
test("parseDataLine should parse valid JSON data with 'data: ' prefix", () => {
|
package/src/stream.ts
CHANGED
|
@@ -2,7 +2,7 @@ export async function* toAsyncIterable(
|
|
|
2
2
|
nodeReadable: NodeJS.ReadableStream,
|
|
3
3
|
): AsyncGenerator<Uint8Array> {
|
|
4
4
|
for await (const chunk of nodeReadable) {
|
|
5
|
-
// @ts-
|
|
5
|
+
// @ts-ignore
|
|
6
6
|
yield chunk as Uint8Array;
|
|
7
7
|
}
|
|
8
8
|
}
|
|
@@ -13,6 +13,7 @@ export async function* streamResponse(
|
|
|
13
13
|
if (response.status === 499) {
|
|
14
14
|
return; // In case of client-side cancellation, just return
|
|
15
15
|
}
|
|
16
|
+
|
|
16
17
|
if (response.status !== 200) {
|
|
17
18
|
throw new Error(await response.text());
|
|
18
19
|
}
|
|
@@ -77,7 +78,7 @@ export function parseDataLine(line: string): any {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function parseSseLine(line: string): { done: boolean; data: any } {
|
|
80
|
-
if (line.startsWith("data: [DONE]")) {
|
|
81
|
+
if (line.startsWith("data:[DONE]") || line.startsWith("data: [DONE]")) {
|
|
81
82
|
return { done: true, data: undefined };
|
|
82
83
|
}
|
|
83
84
|
if (line.startsWith("data:")) {
|
package/tsconfig.json
CHANGED