diff --git a/CHANGELOG.md b/CHANGELOG.md index 1974c63..e92c9d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Added +- Add optional error callbacks to `getJson` and `getHtml`. - Expose `EngineParameters` type. - Expose `InvalidArgumentError` error. diff --git a/README.md b/README.md index a7ffff0..9ec6905 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ Get a JSON response based on search parameters. **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** search query parameters for the engine - `callback` **fn?** optional callback +- `errorCallback` **fn?** optional callback invoked when the request fails #### Examples @@ -206,8 +207,12 @@ Get a JSON response based on search parameters. // single call (async/await) const json = await getJson({ engine: "google", api_key: API_KEY, q: "coffee" }); -// single call (callback) -getJson({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log); +// single call (callback with error handling) +getJson( + { engine: "google", api_key: API_KEY, q: "coffee" }, + console.log, + console.error, +); ``` ### getHtml @@ -223,6 +228,7 @@ Get a HTML response based on search parameters. **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** search query parameters for the engine - `callback` **fn?** optional callback +- `errorCallback` **fn?** optional callback invoked when the request fails #### Examples @@ -230,8 +236,12 @@ Get a HTML response based on search parameters. // async/await const html = await getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }); -// callback -getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log); +// callback with error handling +getHtml( + { engine: "google", api_key: API_KEY, q: "coffee" }, + console.log, + console.error, +); ``` ### getJsonBySearchId diff --git a/src/serpapi.ts b/src/serpapi.ts index 9422ea7..8e2deb8 100644 --- a/src/serpapi.ts +++ b/src/serpapi.ts @@ -14,21 +14,33 @@ const LOCATIONS_PATH = "/locations.json"; const SEARCH_PATH = "/search"; const SEARCH_ARCHIVE_PATH = `/searches`; +type ErrorCallback = (error: unknown) => void; + +function observeError( + promise: Promise, + errorCallback?: ErrorCallback, +): Promise { + if (errorCallback) void promise.catch(errorCallback); + return promise; +} + /** * Get JSON response based on search parameters. * * @param {object} parameters Search query parameters for the engine. Refer to https://serpapi.com/search-api for parameter explanations. * @param {fn=} callback Optional callback. + * @param {fn=} errorCallback Optional callback invoked when the request fails. * @example * // single call (async/await) * const json = await getJson({ engine: "google", api_key: API_KEY, q: "coffee" }); * - * // single call (callback) - * getJson({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log); + * // single call (callback with error handling) + * getJson({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log, console.error); */ export function getJson( parameters: EngineParameters, callback?: (json: BaseResponse) => void, + errorCallback?: ErrorCallback, ): Promise; /** @@ -37,39 +49,50 @@ export function getJson( * @param {string} engine Engine name. Refer to https://serpapi.com/search-api for valid engines. * @param {object} parameters Search query parameters for the engine. Refer to https://serpapi.com/search-api for parameter explanations. * @param {fn=} callback Optional callback. + * @param {fn=} errorCallback Optional callback invoked when the request fails. * @example * // single call (async/await) * const json = await getJson("google", { api_key: API_KEY, q: "coffee" }); * - * // single call (callback) - * getJson("google", { api_key: API_KEY, q: "coffee" }, console.log); + * // single call (callback with error handling) + * getJson("google", { api_key: API_KEY, q: "coffee" }, console.log, console.error); */ export function getJson( engine: string, parameters: EngineParameters, callback?: (json: BaseResponse) => void, + errorCallback?: ErrorCallback, ): Promise; export function getJson( ...args: - | [parameters: EngineParameters, callback?: (json: BaseResponse) => void] + | [ + parameters: EngineParameters, + callback?: (json: BaseResponse) => void, + errorCallback?: ErrorCallback, + ] | [ engine: string, parameters: EngineParameters, callback?: (json: BaseResponse) => void, + errorCallback?: ErrorCallback, ] ): Promise { if (typeof args[0] === "string" && typeof args[1] === "object") { - const [engine, parameters, callback] = args; + const [engine, parameters, callback, errorCallback] = args; const newParameters = { ...parameters, engine } as EngineParameters; - return _getJson(newParameters, callback); + return observeError(_getJson(newParameters, callback), errorCallback); } else if ( typeof args[0] === "object" && typeof args[1] !== "object" && (typeof args[1] === "undefined" || typeof args[1] === "function") ) { - const [parameters, callback] = args; - return _getJson(parameters, callback); + const [parameters, callback, errorCallback] = args as [ + EngineParameters, + ((json: BaseResponse) => void)?, + ErrorCallback?, + ]; + return observeError(_getJson(parameters, callback), errorCallback); } else { throw new InvalidArgumentError(); } @@ -100,16 +123,18 @@ async function _getJson( * * @param {object} parameters Search query parameters for the engine. Refer to https://serpapi.com/search-api for parameter explanations. * @param {fn=} callback Optional callback. + * @param {fn=} errorCallback Optional callback invoked when the request fails. * @example * // async/await * const html = await getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }); * - * // callback - * getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log); + * // callback with error handling + * getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log, console.error); */ export function getHtml( parameters: EngineParameters, callback?: (html: string) => void, + errorCallback?: ErrorCallback, ): Promise; /** @@ -118,39 +143,50 @@ export function getHtml( * @param {string} engine Engine name. Refer to https://serpapi.com/search-api for valid engines. * @param {object} parameters Search query parameters for the engine. Refer to https://serpapi.com/search-api for parameter explanations. * @param {fn=} callback Optional callback. + * @param {fn=} errorCallback Optional callback invoked when the request fails. * @example * // async/await * const html = await getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }); * - * // callback - * getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log); + * // callback with error handling + * getHtml("google", { api_key: API_KEY, q: "coffee" }, console.log, console.error); */ export function getHtml( engine: string, parameters: EngineParameters, callback?: (html: string) => void, + errorCallback?: ErrorCallback, ): Promise; export function getHtml( ...args: - | [parameters: EngineParameters, callback?: (html: string) => void] + | [ + parameters: EngineParameters, + callback?: (html: string) => void, + errorCallback?: ErrorCallback, + ] | [ engine: string, parameters: EngineParameters, callback?: (html: string) => void, + errorCallback?: ErrorCallback, ] ): Promise { if (typeof args[0] === "string" && typeof args[1] === "object") { - const [engine, parameters, callback] = args; + const [engine, parameters, callback, errorCallback] = args; const newParameters = { ...parameters, engine } as EngineParameters; - return _getHtml(newParameters, callback); + return observeError(_getHtml(newParameters, callback), errorCallback); } else if ( typeof args[0] === "object" && typeof args[1] !== "object" && (typeof args[1] === "undefined" || typeof args[1] === "function") ) { - const [parameters, callback] = args; - return _getHtml(parameters, callback); + const [parameters, callback, errorCallback] = args as [ + EngineParameters, + ((html: string) => void)?, + ErrorCallback?, + ]; + return observeError(_getHtml(parameters, callback), errorCallback); } else { throw new InvalidArgumentError(); } diff --git a/tests/serpapi_test.ts b/tests/serpapi_test.ts index 7daf994..9d6d4df 100644 --- a/tests/serpapi_test.ts +++ b/tests/serpapi_test.ts @@ -361,6 +361,67 @@ describe( }, ); + it("error callback", async () => { + const requestError = new Error("request failed"); + const executeStub = stub( + _internals, + "execute", + () => Promise.reject(requestError), + ); + const successCallback = spy((_json: BaseResponse) => {}); + const errorCallback = spy((_error: unknown) => {}); + + try { + await assertRejects( + () => + getJson( + { + engine: "google", + q: "Paris", + api_key: "test_api_key", + }, + successCallback, + errorCallback, + ), + Error, + "request failed", + ); + await assertRejects( + () => + getJson( + "google", + { q: "Paris", api_key: "test_api_key" }, + successCallback, + errorCallback, + ), + Error, + "request failed", + ); + await new Promise((done) => { + void getJson( + { + engine: "google", + q: "Paris", + api_key: "test_api_key", + }, + successCallback, + (error) => { + errorCallback(error); + done(); + }, + ); + }); + } finally { + executeStub.restore(); + } + + assertSpyCalls(successCallback, 0); + assertSpyCalls(errorCallback, 3); + assertSpyCallArg(errorCallback, 0, 0, requestError); + assertSpyCallArg(errorCallback, 1, 0, requestError); + assertSpyCallArg(errorCallback, 2, 0, requestError); + }); + it("rely on global config", async () => { const executeSpy = spy(_internals, "execute"); config.api_key = "test_api_key"; @@ -545,6 +606,67 @@ describe( }, ); + it("error callback", async () => { + const requestError = new Error("request failed"); + const executeStub = stub( + _internals, + "execute", + () => Promise.reject(requestError), + ); + const successCallback = spy((_html: string) => {}); + const errorCallback = spy((_error: unknown) => {}); + + try { + await assertRejects( + () => + getHtml( + { + engine: "google", + q: "Paris", + api_key: "test_api_key", + }, + successCallback, + errorCallback, + ), + Error, + "request failed", + ); + await assertRejects( + () => + getHtml( + "google", + { q: "Paris", api_key: "test_api_key" }, + successCallback, + errorCallback, + ), + Error, + "request failed", + ); + await new Promise((done) => { + void getHtml( + { + engine: "google", + q: "Paris", + api_key: "test_api_key", + }, + successCallback, + (error) => { + errorCallback(error); + done(); + }, + ); + }); + } finally { + executeStub.restore(); + } + + assertSpyCalls(successCallback, 0); + assertSpyCalls(errorCallback, 3); + assertSpyCallArg(errorCallback, 0, 0, requestError); + assertSpyCallArg(errorCallback, 1, 0, requestError); + assertSpyCallArg(errorCallback, 2, 0, requestError); + }); + it("rely on global config", async () => { const executeSpy = spy(_internals, "execute"); config.api_key = "test_api_key";