Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More robust error handling #33

Merged
merged 8 commits into from
Jan 23, 2024
Merged
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
50 changes: 25 additions & 25 deletions src/apis/types/login_types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import { Scope } from "./shared_types.ts";

/**
* ErrorResponse
* Error responses are sent when an error (e.g. unauthorized,
* bad request, etc) occurred.
*
* @example {"error":"invalid_request","error_code":400,
* "error_debug":"The request is missing a required parameter,
* includes an invalid parameter or is otherwise malformed."}
*/
export type LoginErrorResponse = {
/** Name is the error name. */
error: string;
/**
* Code represents the error status code (404, 403, 401, ...).
*
* @format int64
*/
error_code?: number;
/**
* Debug contains debug information.
* This is usually not available and has to be enabled.
*/
error_debug?: string;
};

export type LoginAuthQueryParams = {
/** Value MUST be set to "code". */
response_type: "code";
Expand Down Expand Up @@ -181,28 +206,3 @@ export type LoginWellKnownResponse = {
*/
token_endpoint_auth_methods_supported?: string[];
};

/**
* ErrorResponse
* Error responses are sent when an error (e.g. unauthorized,
* bad request, etc) occurred.
*
* @example {"error":"invalid_request","error_code":400,
* "error_debug":"The request is missing a required parameter,
* includes an invalid parameter or is otherwise malformed."}
*/
export type LoginErrorResponse = {
/** Name is the error name. */
error: string;
/**
* Code represents the error status code (404, 403, 401, ...).
*
* @format int64
*/
error_code?: number;
/**
* Debug contains debug information.
* This is usually not available and has to be enabled.
*/
error_debug?: string;
};
21 changes: 12 additions & 9 deletions src/apis/types/qr_types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { MerchantSerialNumber, ProblemJSON } from "./shared_types.ts";

/**
* Represents the response for a QR error.
*/
export type QrErrorResponse = ProblemJSON & {
invalidParams?: {
/** @minLength 1 */
name: string;
/** @minLength 1 */
reason: string;
}[];
};

/**
* @description Requested image format.
* Supported values: {image/*,image/png, image/svg+xml, text/targetUrl}
Expand Down Expand Up @@ -145,12 +157,3 @@ export type QrWebhookEvent = {
*/
initiatedAt: string;
};

export type QrErrorResponse = ProblemJSON & {
invalidParams?: {
/** @minLength 1 */
name: string;
/** @minLength 1 */
reason: string;
}[];
};
46 changes: 23 additions & 23 deletions src/apis/types/recurring_types.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
//////////////// Common types /////////////////

import { ProblemJSON, Scope } from "./shared_types.ts";

/**
* Only NOK is supported at the moment. Support for EUR and DKK will be provided in early 2024.
* @minLength 3
* @maxLength 3
* @pattern ^[A-Z]{3}$
* @example "NOK"
*/
export type RecurringCurrencyV3 = "NOK";

/**
* @default "RECURRING"
* @example "RECURRING"
*/
export type ChargeType = "INITIAL" | "RECURRING";

/**
* Type of transaction, either direct capture or reserve capture
* @example "DIRECT_CAPTURE"
*/
export type RecurringTransactionType = "DIRECT_CAPTURE" | "RESERVE_CAPTURE";

///////////////// Error types /////////////////

export type RecurringErrorResponse = RecurringErrorV3 | RecurringErrorFromAzure;
Expand Down Expand Up @@ -68,6 +45,29 @@ export type RecurringErrorFromAzure = {
};
};

//////////////// Common types /////////////////

/**
* Only NOK is supported at the moment. Support for EUR and DKK will be provided in early 2024.
* @minLength 3
* @maxLength 3
* @pattern ^[A-Z]{3}$
* @example "NOK"
*/
export type RecurringCurrencyV3 = "NOK";

/**
* @default "RECURRING"
* @example "RECURRING"
*/
export type ChargeType = "INITIAL" | "RECURRING";

/**
* Type of transaction, either direct capture or reserve capture
* @example "DIRECT_CAPTURE"
*/
export type RecurringTransactionType = "DIRECT_CAPTURE" | "RESERVE_CAPTURE";

//////////////// Charge types /////////////////

export type CreateChargeV3Request = {
Expand Down
6 changes: 6 additions & 0 deletions src/apis/types/shared_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,9 @@ export type ProblemJSON = {
*/
instance?: string | null;
};

export type SDKError<TErr> = {
ok: false;
message: string;
error?: TErr;
};
3 changes: 2 additions & 1 deletion src/apis/types/webhooks_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { ProblemJSON } from "./shared_types.ts";

// Error types //
export type WebhooksErrorResponse = ProblemJSON & {
extraDetails: {
traceId?: string | null;
extraDetails?: {
name: string;
reason: string;
}[] | null;
Expand Down
11 changes: 9 additions & 2 deletions src/base_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
ClientResponse,
RequestData,
} from "./types.ts";
import { buildRequest, fetchRetry } from "./base_client_helper.ts";
import { parseError } from "./errors.ts";
import { buildRequest } from "./base_client_helper.ts";
import { parseError, parseRetryError } from "./errors.ts";
import { validateRequestData } from "./validate.ts";
import { fetchRetry } from "./fetch.ts";
import { isRetryError } from "./errors.ts";

/**
* Creates a base client with the given configuration.
Expand Down Expand Up @@ -37,6 +39,11 @@ export const baseClient = (cfg: ClientConfig): BaseClient =>
const res = await fetchRetry<TOk, TErr>(request, cfg.retryRequests);
return res;
} catch (error: unknown) {
//Check if the error is a RetryError.
if (isRetryError(error)) {
return parseRetryError();
}
// Otherwise, parse the error.
return parseError<TErr>(error);
}
},
Expand Down
77 changes: 7 additions & 70 deletions src/base_client_helper.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,14 @@
import { filterKeys, isErrorStatus, retry } from "./deps.ts";
import { parseError } from "./errors.ts";
import { filterKeys } from "./deps.ts";
import {
ClientConfig,
ClientResponse,
DefaultHeaders,
OmitHeaders,
RequestData,
} from "./types.ts";

/**
* Executes a fetch request with optional retry logic.
*
* @param request - The request to be executed.
* @param retryRequest - Whether to retry the request if it fails. Default is true.
* @returns A promise that resolves to the response of the request.
*/
export const fetchRetry = async <TOk, TErr>(
request: Request,
retryRequest = true,
): Promise<ClientResponse<TOk, TErr>> => {
// Execute request without retry
if (!retryRequest) {
return await fetchJSON<TOk, TErr>(request);
}
// Execute request using retry
const req = retry(async () => {
return await fetchJSON<TOk, TErr>(request);
}, {
multiplier: 2,
maxTimeout: 3000,
maxAttempts: 3,
minTimeout: 1000,
jitter: 0,
});
return req;
};

/**
* Fetches JSON data from the specified request.
* @param request - The request to fetch JSON data from.
* @returns A promise that resolves to a ClientResponse object containing the fetched data.
* @template TOk - The type of the successful response data.
* @template TErr - The type of the error response data.
*/
export const fetchJSON = async <TOk, TErr>(
request: Request,
): Promise<ClientResponse<TOk, TErr>> => {
const response = await fetch(request);

if (isErrorStatus(response.status)) {
// Bad Request
if (response.status === 400) {
const error = await response.json();
return parseError<TErr>(error);
} else {
// Throwing an error here will trigger a retry
throw new Error(response.statusText);
}
}
const text = await response.text();
// Handle empty response body
if (text === "") {
return { ok: true, data: {} as TOk };
}
const json = JSON.parse(text);
return { ok: true, data: json as TOk };
};

/**
* Builds a Request object based on the provided configuration and request data.
*
* @param cfg - The client configuration.
* @param requestData - The request data containing method, headers, token, body, and URL.
* @returns A Request object.
Expand Down Expand Up @@ -141,7 +81,8 @@ export const getHeaders = (
* @returns The user agent string.
*/
export const getUserAgent = (): string => {
const metaUrl = import.meta.url || undefined;
const metaUrl: string | undefined = import.meta.url;

// If the sdk is loaded using require, import.meta.url will be undefined
if (!metaUrl) {
return "Vipps/Deno SDK/npm-require";
Expand All @@ -160,21 +101,17 @@ export const getUserAgent = (): string => {
export const createSDKUserAgent = (metaUrl: string): string => {
const url = new URL(metaUrl);

let userAgent = "Vipps/Deno SDK/";
// Check if the module was loaded from deno.land
if (
url.host === "deno.land" &&
url.pathname.includes("vipps_mobilepay_sdk")
) {
// Extract the module version from the URL
const sdkVersion = url.pathname.split("@")[1].split("/")[0];
userAgent += sdkVersion;
return `Vipps/Deno SDK/${sdkVersion}`;
} // Or if the module was loaded from npm
else if (url.pathname.includes("node_modules")) {
userAgent += "npm-module";
return `Vipps/Deno SDK/npm-module`;
} // Otherwise, we don't know where the module was loaded from
else {
userAgent += "unknown";
}
return userAgent;
return `Vipps/Deno SDK/unknown`;
};
6 changes: 5 additions & 1 deletion src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ export {
retry,
RetryError,
} from "https://deno.land/[email protected]/async/retry.ts";
export { isErrorStatus } from "https://deno.land/[email protected]/http/status.ts";
export {
isServerErrorStatus,
isSuccessfulStatus,
STATUS_CODE,
} from "https://deno.land/[email protected]/http/status.ts";
export { filterKeys } from "https://deno.land/[email protected]/collections/mod.ts";
Loading