Skip to content

Commit 3248a14

Browse files
committed
Informed error parsing from api
1 parent 4082b65 commit 3248a14

File tree

6 files changed

+122
-22
lines changed

6 files changed

+122
-22
lines changed

.changeset/kind-snails-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
Informed error parsing from api

packages/core/lib/v3/api.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,9 @@ export class StagehandAPIClient {
415415

416416
if (eventData.type === "system") {
417417
if (eventData.data.status === "error") {
418-
throw new StagehandServerError(eventData.data.error);
418+
const { error: errorMsg } = eventData.data;
419+
// Throw plain Error to match local SDK behavior (useApi: false)
420+
throw new Error(errorMsg);
419421
}
420422
if (eventData.data.status === "finished") {
421423
return eventData.data.result as T;
@@ -424,8 +426,9 @@ export class StagehandAPIClient {
424426
this.logger(eventData.data.message);
425427
}
426428
} catch (e) {
427-
// Don't catch and re-throw StagehandServerError
428-
if (e instanceof StagehandServerError) {
429+
// Let Error instances pass through (server errors thrown above)
430+
// Only wrap SyntaxError from JSON.parse as parse errors
431+
if (e instanceof Error && !(e instanceof SyntaxError)) {
429432
throw e;
430433
}
431434

packages/core/lib/v3/types/public/apiErrors.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,45 @@ export class StagehandResponseParseError extends StagehandAPIError {
3434
super(message);
3535
}
3636
}
37+
38+
/**
39+
* Enhanced error class for Stagehand operation failures.
40+
* Includes error code and operation name for better debugging.
41+
*/
42+
export class StagehandOperationError extends StagehandAPIError {
43+
public readonly code?: string;
44+
public readonly operation?: string;
45+
46+
constructor(data: { error: string; code?: string; operation?: string }) {
47+
super(data.error);
48+
this.code = data.code;
49+
this.operation = data.operation;
50+
}
51+
52+
/**
53+
* Returns true if the error is a user error (bad input, invalid arguments).
54+
* User errors can typically be fixed by changing the request.
55+
*/
56+
isUserError(): boolean {
57+
return [
58+
"INVALID_ARGUMENT",
59+
"MISSING_ARGUMENT",
60+
"INVALID_MODEL",
61+
"INVALID_SCHEMA",
62+
"EXPERIMENTAL_NOT_CONFIGURED",
63+
].includes(this.code ?? "");
64+
}
65+
66+
/**
67+
* Returns true if the operation might succeed on retry.
68+
* These are transient failures that may resolve themselves.
69+
*/
70+
isRetryable(): boolean {
71+
return [
72+
"ACTION_FAILED",
73+
"TIMEOUT",
74+
"LLM_ERROR",
75+
"ELEMENT_NOT_FOUND",
76+
].includes(this.code ?? "");
77+
}
78+
}

packages/core/lib/v3/v3.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -530,34 +530,29 @@ export class V3 {
530530
await Promise.all(instances.map((i) => i._immediateShutdown(reason)));
531531
};
532532

533-
process.once("SIGINT", () => {
533+
const handleSignal = async (signal: string) => {
534534
v3Logger({
535535
category: "v3",
536-
message: "SIGINT: initiating shutdown",
537-
level: 0,
538-
});
539-
for (const instance of V3._instances) {
540-
if (instance.apiClient) {
541-
void instance.apiClient.end();
542-
return;
543-
}
544-
}
545-
void shutdownAllImmediate("signal SIGINT");
546-
});
547-
process.once("SIGTERM", () => {
548-
v3Logger({
549-
category: "v3",
550-
message: "SIGTERM: initiating shutdown",
536+
message: `${signal}: initiating shutdown`,
551537
level: 0,
552538
});
539+
540+
// In API mode, let the server handle cleanup - don't close locally
553541
for (const instance of V3._instances) {
554542
if (instance.apiClient) {
555-
void instance.apiClient.end();
543+
const timeout = new Promise((r) => setTimeout(r, 5000));
544+
await Promise.race([
545+
instance.apiClient.end().catch(() => {}),
546+
timeout,
547+
]);
556548
return;
557549
}
558550
}
559-
void shutdownAllImmediate("signal SIGTERM");
560-
});
551+
await shutdownAllImmediate(`signal ${signal}`);
552+
};
553+
554+
process.once("SIGINT", () => void handleSignal("SIGINT"));
555+
process.once("SIGTERM", () => void handleSignal("SIGTERM"));
561556
process.once("uncaughtException", (err: unknown) => {
562557
v3Logger({
563558
category: "v3",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from "vitest";
2+
import { StagehandResponseParseError } from "../lib/v3/types/public/apiErrors";
3+
4+
describe("API Error Handling", () => {
5+
describe("SSE error parsing", () => {
6+
it("throws plain Error with raw message for server errors", () => {
7+
// Simulate what happens in api.ts when SSE error is received
8+
const serverErrorMessage =
9+
"API key not valid. Please pass a valid API key.";
10+
const eventData = {
11+
type: "system",
12+
data: {
13+
status: "error",
14+
error: serverErrorMessage,
15+
},
16+
};
17+
18+
// This simulates the error handling logic in api.ts execute()
19+
if (eventData.data.status === "error") {
20+
const { error: errorMsg } = eventData.data;
21+
const thrownError = new Error(errorMsg);
22+
23+
// Verify it's a plain Error, not a wrapped type
24+
expect(thrownError).toBeInstanceOf(Error);
25+
expect(thrownError.constructor.name).toBe("Error");
26+
expect(thrownError.message).toBe(serverErrorMessage);
27+
}
28+
});
29+
30+
it("wraps SyntaxError in StagehandResponseParseError for JSON parse failures", () => {
31+
const invalidJson = "not valid json {";
32+
33+
try {
34+
JSON.parse(invalidJson);
35+
} catch (e) {
36+
// This simulates the catch block logic in api.ts
37+
if (e instanceof Error && !(e instanceof SyntaxError)) {
38+
throw e; // Would pass through
39+
}
40+
41+
// SyntaxError gets wrapped
42+
const errorMessage = e instanceof Error ? e.message : String(e);
43+
const wrappedError = new StagehandResponseParseError(
44+
`Failed to parse server response: ${errorMessage}`,
45+
);
46+
47+
expect(wrappedError).toBeInstanceOf(StagehandResponseParseError);
48+
expect(wrappedError.message).toContain(
49+
"Failed to parse server response",
50+
);
51+
}
52+
});
53+
});
54+
});

packages/core/tests/public-api/public-error-types.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const publicErrorTypes = {
3737
StagehandInvalidArgumentError: Stagehand.StagehandInvalidArgumentError,
3838
StagehandMissingArgumentError: Stagehand.StagehandMissingArgumentError,
3939
StagehandNotInitializedError: Stagehand.StagehandNotInitializedError,
40+
StagehandOperationError: Stagehand.StagehandOperationError,
4041
StagehandResponseBodyError: Stagehand.StagehandResponseBodyError,
4142
StagehandResponseParseError: Stagehand.StagehandResponseParseError,
4243
StagehandServerError: Stagehand.StagehandServerError,

0 commit comments

Comments
 (0)