Skip to content

Commit da7ed0e

Browse files
authored
Introduce a new error type to represent AbortError (google-gemini#357)
* add a new error to represent AbortError from SDK * changesets * format * fix imports order * update docs * Revert "update docs" This reverts commit d00e6eb. * api docs * catch AbortError from stream reader * format * test * fix import
1 parent 9e95663 commit da7ed0e

File tree

10 files changed

+171
-30
lines changed

10 files changed

+171
-30
lines changed

.changeset/tricky-glasses-open.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@google/generative-ai": minor
3+
---
4+
5+
Introduce a new error type to represent AbortError from SDK

common/api-review/generative-ai.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,10 @@ export class GoogleGenerativeAI {
539539
getGenerativeModelFromCachedContent(cachedContent: CachedContent, modelParams?: Partial<ModelParams>, requestOptions?: RequestOptions): GenerativeModel;
540540
}
541541

542+
// @public
543+
export class GoogleGenerativeAIAbortError extends GoogleGenerativeAIError {
544+
}
545+
542546
// @public
543547
export class GoogleGenerativeAIError extends Error {
544548
constructor(message: string);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [@google/generative-ai](./generative-ai.md) &gt; [GoogleGenerativeAIAbortError](./generative-ai.googlegenerativeaiaborterror.md)
4+
5+
## GoogleGenerativeAIAbortError class
6+
7+
Error thrown when a request is aborted, either due to a timeout or intentional cancellation by the user.
8+
9+
**Signature:**
10+
11+
```typescript
12+
export declare class GoogleGenerativeAIAbortError extends GoogleGenerativeAIError
13+
```
14+
**Extends:** [GoogleGenerativeAIError](./generative-ai.googlegenerativeaierror.md)
15+

docs/reference/main/generative-ai.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
| [ChatSession](./generative-ai.chatsession.md) | ChatSession class that enables sending chat messages and stores history of sent and received messages so far. |
1212
| [GenerativeModel](./generative-ai.generativemodel.md) | Class for generative model APIs. |
1313
| [GoogleGenerativeAI](./generative-ai.googlegenerativeai.md) | Top-level class for this SDK |
14+
| [GoogleGenerativeAIAbortError](./generative-ai.googlegenerativeaiaborterror.md) | Error thrown when a request is aborted, either due to a timeout or intentional cancellation by the user. |
1415
| [GoogleGenerativeAIError](./generative-ai.googlegenerativeaierror.md) | Basic error type for this SDK. |
1516
| [GoogleGenerativeAIFetchError](./generative-ai.googlegenerativeaifetcherror.md) | Error class covering HTTP errors when calling the server. Includes HTTP status, statusText, and optional details, if provided in the server response. |
1617
| [GoogleGenerativeAIRequestInputError](./generative-ai.googlegenerativeairequestinputerror.md) | Errors in the contents of a request originating from user input. |

src/errors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,10 @@ export class GoogleGenerativeAIFetchError extends GoogleGenerativeAIError {
6464
* @public
6565
*/
6666
export class GoogleGenerativeAIRequestInputError extends GoogleGenerativeAIError {}
67+
68+
/**
69+
* Error thrown when a request is aborted, either due to a timeout or
70+
* intentional cancellation by the user.
71+
* @public
72+
*/
73+
export class GoogleGenerativeAIAbortError extends GoogleGenerativeAIError {}

src/requests/request.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
makeModelRequest,
2929
} from "./request";
3030
import {
31+
GoogleGenerativeAIAbortError,
3132
GoogleGenerativeAIFetchError,
3233
GoogleGenerativeAIRequestInputError,
3334
} from "../errors";
@@ -228,7 +229,30 @@ describe("request methods", () => {
228229
});
229230
expect(response.ok).to.be.true;
230231
});
231-
it("error with timeout", async () => {
232+
it("error with local timeout", async () => {
233+
const abortError = new DOMException("Request timeout.", "AbortError");
234+
const fetchStub = stub().rejects(abortError);
235+
236+
try {
237+
await makeModelRequest(
238+
"model-name",
239+
Task.GENERATE_CONTENT,
240+
"key",
241+
true,
242+
"",
243+
{
244+
timeout: 100,
245+
},
246+
fetchStub as typeof fetch,
247+
);
248+
} catch (e) {
249+
expect((e as GoogleGenerativeAIAbortError).message).to.include(
250+
"Request aborted",
251+
);
252+
}
253+
expect(fetchStub).to.be.calledOnce;
254+
});
255+
it("error with server timeout", async () => {
232256
const fetchStub = stub().resolves({
233257
ok: false,
234258
status: 500,

src/requests/request.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { RequestOptions, SingleRequestOptions } from "../../types";
1919
import {
20+
GoogleGenerativeAIAbortError,
2021
GoogleGenerativeAIError,
2122
GoogleGenerativeAIFetchError,
2223
GoogleGenerativeAIRequestInputError,
@@ -172,7 +173,12 @@ export async function makeRequest(
172173

173174
function handleResponseError(e: Error, url: string): void {
174175
let err = e;
175-
if (
176+
if (err.name === "AbortError") {
177+
err = new GoogleGenerativeAIAbortError(
178+
`Request aborted when fetching ${url.toString()}: ${e.message}`,
179+
);
180+
err.stack = e.stack;
181+
} else if (
176182
!(
177183
e instanceof GoogleGenerativeAIFetchError ||
178184
e instanceof GoogleGenerativeAIRequestInputError

src/requests/stream-reader.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { restore } from "sinon";
2525
import * as sinonChai from "sinon-chai";
2626
import {
2727
getChunkedStream,
28+
getErrorStream,
2829
getMockResponseStreaming,
2930
} from "../../test-utils/mock-response";
3031
import {
@@ -34,6 +35,10 @@ import {
3435
HarmCategory,
3536
HarmProbability,
3637
} from "../../types";
38+
import {
39+
GoogleGenerativeAIAbortError,
40+
GoogleGenerativeAIError,
41+
} from "../errors";
3742

3843
use(sinonChai);
3944

@@ -61,6 +66,48 @@ describe("getResponseStream", () => {
6166
}
6267
expect(responses).to.deep.equal(src);
6368
});
69+
it("throw AbortError", async () => {
70+
const inputStream = getErrorStream(
71+
new DOMException("Simulated AbortError", "AbortError"),
72+
).pipeThrough(new TextDecoderStream("utf8", { fatal: true }));
73+
const responseStream = getResponseStream<{ text: string }>(inputStream);
74+
const reader = responseStream.getReader();
75+
const responses: Array<{ text: string }> = [];
76+
try {
77+
while (true) {
78+
const { done, value } = await reader.read();
79+
if (done) {
80+
break;
81+
}
82+
responses.push(value);
83+
}
84+
} catch (e) {
85+
expect((e as GoogleGenerativeAIAbortError).message).to.include(
86+
"Request aborted",
87+
);
88+
}
89+
});
90+
it("throw non AbortError", async () => {
91+
const inputStream = getErrorStream(
92+
new DOMException("Simulated Error", "RandomError"),
93+
).pipeThrough(new TextDecoderStream("utf8", { fatal: true }));
94+
const responseStream = getResponseStream<{ text: string }>(inputStream);
95+
const reader = responseStream.getReader();
96+
const responses: Array<{ text: string }> = [];
97+
try {
98+
while (true) {
99+
const { done, value } = await reader.read();
100+
if (done) {
101+
break;
102+
}
103+
responses.push(value);
104+
}
105+
} catch (e) {
106+
expect((e as GoogleGenerativeAIError).message).to.include(
107+
"Error reading from the stream",
108+
);
109+
}
110+
});
64111
});
65112

66113
describe("processStream", () => {

src/requests/stream-reader.ts

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import {
2222
GenerateContentStreamResult,
2323
Part,
2424
} from "../../types";
25-
import { GoogleGenerativeAIError } from "../errors";
25+
import {
26+
GoogleGenerativeAIAbortError,
27+
GoogleGenerativeAIError,
28+
} from "../errors";
2629
import { addHelpers } from "./response-helpers";
2730

2831
const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
@@ -89,38 +92,54 @@ export function getResponseStream<T>(
8992
let currentText = "";
9093
return pump();
9194
function pump(): Promise<(() => Promise<void>) | undefined> {
92-
return reader.read().then(({ value, done }) => {
93-
if (done) {
94-
if (currentText.trim()) {
95-
controller.error(
96-
new GoogleGenerativeAIError("Failed to parse stream"),
97-
);
95+
return reader
96+
.read()
97+
.then(({ value, done }) => {
98+
if (done) {
99+
if (currentText.trim()) {
100+
controller.error(
101+
new GoogleGenerativeAIError("Failed to parse stream"),
102+
);
103+
return;
104+
}
105+
controller.close();
98106
return;
99107
}
100-
controller.close();
101-
return;
102-
}
103108

104-
currentText += value;
105-
let match = currentText.match(responseLineRE);
106-
let parsedResponse: T;
107-
while (match) {
108-
try {
109-
parsedResponse = JSON.parse(match[1]);
110-
} catch (e) {
111-
controller.error(
112-
new GoogleGenerativeAIError(
113-
`Error parsing JSON response: "${match[1]}"`,
114-
),
109+
currentText += value;
110+
let match = currentText.match(responseLineRE);
111+
let parsedResponse: T;
112+
while (match) {
113+
try {
114+
parsedResponse = JSON.parse(match[1]);
115+
} catch (e) {
116+
controller.error(
117+
new GoogleGenerativeAIError(
118+
`Error parsing JSON response: "${match[1]}"`,
119+
),
120+
);
121+
return;
122+
}
123+
controller.enqueue(parsedResponse);
124+
currentText = currentText.substring(match[0].length);
125+
match = currentText.match(responseLineRE);
126+
}
127+
return pump();
128+
})
129+
.catch((e: Error) => {
130+
let err = e;
131+
err.stack = e.stack;
132+
if (err.name === "AbortError") {
133+
err = new GoogleGenerativeAIAbortError(
134+
"Request aborted when reading from the stream",
135+
);
136+
} else {
137+
err = new GoogleGenerativeAIError(
138+
"Error reading from the stream",
115139
);
116-
return;
117140
}
118-
controller.enqueue(parsedResponse);
119-
currentText = currentText.substring(match[0].length);
120-
match = currentText.match(responseLineRE);
121-
}
122-
return pump();
123-
});
141+
throw err;
142+
});
124143
}
125144
},
126145
});

test-utils/mock-response.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ export function getChunkedStream(
4848

4949
return stream;
5050
}
51+
52+
/**
53+
* Returns a stream which would throw the given error.
54+
*/
55+
export function getErrorStream(err: Error): ReadableStream<Uint8Array> {
56+
const stream = new ReadableStream<Uint8Array>({
57+
start(controller) {
58+
controller.error(err);
59+
},
60+
});
61+
return stream;
62+
}
63+
5164
export function getMockResponseStreaming(
5265
filename: string,
5366
chunkLength: number = 20,

0 commit comments

Comments
 (0)