Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to
`getMdBySearchId`.
- Expose `EngineParameters` type.
- Expose `InvalidArgumentError` error.
- Expose `HTTPError` error raised on non-200 responses, with `statusCode` and
`body` properties.

### Changed

Expand All @@ -23,6 +25,9 @@ and this project adheres to

### Fixed

- Reject with an `HTTPError` instead of the raw response body when SerpApi
responds with a non-200 status code.

### Removed

- Remove all types for engine parameters and responses. SerpApi's
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,30 @@ await getJson({ engine: "google", q: "coffee" }); // uses the API key defined in
await getJson({ engine: "google", api_key: API_KEY_2, q: "coffee" }); // API_KEY_2 will be used
```

### Error handling

When SerpApi responds with a non-200 status code, the returned promise rejects
with an `HTTPError`. It exposes the response `statusCode` and the raw `body`,
and its message is set to the `error` field of the response when one is present.

```js
import { getJson, HTTPError } from "serpapi";

try {
const json = await getJson({
engine: "google",
api_key: API_KEY,
q: "coffee",
});
} catch (error) {
if (error instanceof HTTPError) {
console.log(error.statusCode); // e.g. 401
console.log(error.body); // e.g. '{"error":"Invalid API key. ..."}'
console.log(error.message); // e.g. 'Invalid API key. ...'
}
}
```

### Using a Proxy

> **Note:** SerpApi handles proxies on its end — you do **not** need to supply
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type { Config } from "./src/config.ts";
export { config } from "./src/config.ts";

export {
HTTPError,
InvalidArgumentError,
InvalidTimeoutError,
MissingApiKeyError,
Expand Down
37 changes: 37 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,40 @@ export class RequestTimeoutError extends Error {
Object.setPrototypeOf(this, RequestTimeoutError.prototype);
}
}

/**
* Error raised when SerpApi responds with a non-200 status code.
*
* @property {number} statusCode HTTP status code of the response.
* @property {string} body Raw response body.
* @example
* try {
* const json = await getJson({ engine: "google", api_key: API_KEY, q: "coffee" });
* } catch (error) {
* if (error instanceof HTTPError) {
* console.log(error.statusCode, error.body);
* }
* }
*/
export class HTTPError extends Error {
readonly statusCode: number | undefined;
readonly body: string;

constructor(statusCode: number | undefined, body: string) {
let message = `Request failed with status code ${statusCode}`;
try {
const parsed = JSON.parse(body) as { error?: string };
if (
parsed && typeof parsed.error === "string" && parsed.error.length > 0
) {
message = parsed.error;
}
} catch {
// The body is not JSON. Fall back to the status code message.
}
super(message);
this.statusCode = statusCode;
this.body = body;
Object.setPrototypeOf(this, HTTPError.prototype);
}
}
6 changes: 3 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import https from "node:https";
import http from "node:http";
import qs from "node:querystring";
import process from "node:process";
import { RequestTimeoutError } from "./errors.ts";
import { HTTPError, RequestTimeoutError } from "./errors.ts";
import { config } from "./config.ts";
import { createMultipartBody } from "./multipart.ts";

Expand Down Expand Up @@ -99,7 +99,7 @@ export function execute(
if (resp.statusCode == 200) {
resolve(data);
} else {
reject(data);
reject(new HTTPError(resp.statusCode, data));
}
} catch (e) {
reject(e);
Expand Down Expand Up @@ -170,7 +170,7 @@ export function uploadImage(
resp.on("end", () => {
if (timer) clearTimeout(timer);
if (resp.statusCode === 200) resolve(data);
else reject(data);
else reject(new HTTPError(resp.statusCode, data));
});
});
req.on("error", (error) => {
Expand Down
49 changes: 49 additions & 0 deletions tests/errors_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it } from "@std/testing/bdd";
import { assertEquals, assertInstanceOf } from "@std/testing/asserts";
import { HTTPError } from "../src/errors.ts";

describe("HTTPError", () => {
it("with JSON body containing an error field", () => {
const error = new HTTPError(
401,
'{"error":"Invalid API key. Your API key should be here: https://serpapi.com/manage-api-key"}',
);
assertInstanceOf(error, HTTPError);
assertInstanceOf(error, Error);
assertEquals(error.statusCode, 401);
assertEquals(
error.body,
'{"error":"Invalid API key. Your API key should be here: https://serpapi.com/manage-api-key"}',
);
assertEquals(
error.message,
"Invalid API key. Your API key should be here: https://serpapi.com/manage-api-key",
);
});

it("with JSON body without an error field", () => {
const error = new HTTPError(404, '{"foo":"bar"}');
assertEquals(error.statusCode, 404);
assertEquals(error.body, '{"foo":"bar"}');
assertEquals(error.message, "Request failed with status code 404");
});

it("with non-JSON body", () => {
const error = new HTTPError(500, "Internal Server Error");
assertEquals(error.statusCode, 500);
assertEquals(error.body, "Internal Server Error");
assertEquals(error.message, "Request failed with status code 500");
});

it("with empty JSON error field", () => {
const error = new HTTPError(400, '{"error":""}');
assertEquals(error.message, "Request failed with status code 400");
});

it("with empty body", () => {
const error = new HTTPError(502, "");
assertEquals(error.statusCode, 502);
assertEquals(error.body, "");
assertEquals(error.message, "Request failed with status code 502");
});
});
8 changes: 6 additions & 2 deletions tests/serpapi_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
getLocations,
getMd,
getMdBySearchId,
HTTPError,
InvalidArgumentError,
InvalidTimeoutError,
MissingApiKeyError,
Expand Down Expand Up @@ -248,12 +249,15 @@ describe("uploadImage", () => {
const executeStub = stub(
_internals,
"uploadImage",
() => Promise.reject(apiError),
() => Promise.reject(new HTTPError(401, apiError)),
);
config.api_key = "test_api_key";
try {
const error = await uploadImage({ image }).catch((error) => error);
assertEquals(error, apiError);
assertInstanceOf(error, HTTPError);
assertEquals(error.statusCode, 401);
assertEquals(error.body, apiError);
assertEquals(error.message, "Invalid image");
} finally {
executeStub.restore();
}
Expand Down
17 changes: 16 additions & 1 deletion tests/utils_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
execute,
getSource,
} from "../src/utils.ts";
import { RequestTimeoutError } from "../src/errors.ts";
import { HTTPError, RequestTimeoutError } from "../src/errors.ts";
import { Config, config } from "../src/config.ts";

loadSync({ export: true });
Expand Down Expand Up @@ -233,5 +233,20 @@ describe(
assertInstanceOf(e, RequestTimeoutError);
}
});

it("with error response", async () => {
const error = await execute(
"/account",
{ api_key: "invalid_api_key" },
20000,
).catch((error) => error);
assertInstanceOf(error, HTTPError);
assertEquals(error.statusCode, 401);
assertMatch(error.body, /"error"/);
assertEquals(
error.message,
"Invalid API key. Your API key should be here: https://serpapi.com/manage-api-key",
);
});
},
);